├── pong.jpeg ├── sounds ├── music.mp3 ├── score.mp3 ├── combo_hit.mp3 ├── game_over.mp3 ├── multiball.mp3 ├── wall_hit.mp3 ├── bg_music_1.mp3 ├── bg_music_2.mp3 ├── bg_music_3.mp3 ├── paddle_hit.mp3 ├── player_win.mp3 ├── countdown_beep.mp3 ├── powerup_activate.mp3 └── powerup_collect.mp3 ├── package.json ├── LICENSE ├── DROPDOWN_FIX.md ├── audio.js ├── themes.js ├── Code_of_Conduct.md ├── theme-test.html ├── CONTRIBUTING.md ├── IMPLEMENTATION_SUMMARY.md ├── THEME_DOCUMENTATION.md ├── index.html ├── README.md ├── theme.js ├── themes.css ├── script.js └── style.css /pong.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/pong.jpeg -------------------------------------------------------------------------------- /sounds/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/music.mp3 -------------------------------------------------------------------------------- /sounds/score.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/score.mp3 -------------------------------------------------------------------------------- /sounds/combo_hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/combo_hit.mp3 -------------------------------------------------------------------------------- /sounds/game_over.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/game_over.mp3 -------------------------------------------------------------------------------- /sounds/multiball.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/multiball.mp3 -------------------------------------------------------------------------------- /sounds/wall_hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/wall_hit.mp3 -------------------------------------------------------------------------------- /sounds/bg_music_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/bg_music_1.mp3 -------------------------------------------------------------------------------- /sounds/bg_music_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/bg_music_2.mp3 -------------------------------------------------------------------------------- /sounds/bg_music_3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/bg_music_3.mp3 -------------------------------------------------------------------------------- /sounds/paddle_hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/paddle_hit.mp3 -------------------------------------------------------------------------------- /sounds/player_win.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/player_win.mp3 -------------------------------------------------------------------------------- /sounds/countdown_beep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/countdown_beep.mp3 -------------------------------------------------------------------------------- /sounds/powerup_activate.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/powerup_activate.mp3 -------------------------------------------------------------------------------- /sounds/powerup_collect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akki-jaiswal/pong-game/HEAD/sounds/powerup_collect.mp3 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pong-game", 3 | "version": "1.0.0", 4 | "description": "Relive the nostalgia of a retro classic! This project is a straightforward yet engaging implementation of the iconic Pong game, built using core web technologies: HTML, CSS, and JavaScript. It features an AI opponent, adjustable difficulty, interactive elements, full responsiveness, and touch controls to provide a fun and familiar experience for all players.", 5 | "main": "audio.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Rockyoudead8/pong-game.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Rockyoudead8/pong-game/issues" 17 | }, 18 | "homepage": "https://github.com/Rockyoudead8/pong-game#readme" 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Akshay Jaiswal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DROPDOWN_FIX.md: -------------------------------------------------------------------------------- 1 | # 🔧 Theme Dropdown Styling Fix 2 | 3 | ## Issue Fixed 4 | 5 | The theme dropdown selector was displaying with white/default colors instead of adapting to the current theme colors. 6 | 7 | ## Solution Implemented 8 | 9 | ### 1. **Enhanced CSS Styling** 10 | 11 | - Added `!important` declarations to ensure theme colors override browser defaults 12 | - Implemented cross-browser compatibility with `-webkit-appearance: none` 13 | - Added custom dropdown arrow with theme-appropriate colors 14 | - Enhanced focus and hover states 15 | 16 | ### 2. **Theme-Specific Dropdown Colors** 17 | 18 | Each theme now has properly styled dropdown options: 19 | 20 | - **Default**: Translucent white with blue accents 21 | - **Neon Retro**: Electric purple background with cyan text 22 | - **Dark Mode**: Gray background with white text 23 | - **Ocean Blue**: Blue background with aqua text 24 | 25 | ### 3. **JavaScript Enhancement** 26 | 27 | - Added `updateDropdownStyling()` method in theme.js 28 | - Forces style refresh when themes are switched 29 | - Ensures proper styling across all browsers 30 | 31 | ### 4. **Cross-Browser Compatibility** 32 | 33 | - Uses CSS custom properties with fallbacks 34 | - Implements browser-specific prefixes 35 | - Added SVG-based dropdown arrows with theme colors 36 | 37 | ## Result 38 | 39 | ✅ Dropdown now perfectly matches the selected theme 40 | ✅ Consistent styling across all browsers 41 | ✅ Smooth transitions when switching themes 42 | ✅ Enhanced accessibility and visual coherence 43 | 44 | The theme selector dropdown now seamlessly integrates with each theme's color scheme, providing a cohesive and polished user experience! 45 | -------------------------------------------------------------------------------- /audio.js: -------------------------------------------------------------------------------- 1 | // audio.js 2 | export const paddleHitSound = new Audio('sounds/paddle_hit.mp3'); 3 | export const wallHitSound = new Audio('sounds/wall_hit.mp3'); 4 | export const scoreSound = new Audio('sounds/score.mp3'); 5 | export const gameOverSound = new Audio('sounds/game_over.mp3'); 6 | export const playerWinSound = new Audio('sounds/player_win.mp3'); 7 | export const countdownBeepSound = new Audio('sounds/countdown_beep.mp3'); 8 | 9 | // Background Music Tracks - ensure paths are correct 10 | export const backgroundMusicTracks = [ 11 | new Audio('sounds/bg_music_1.mp3'), 12 | new Audio('sounds/bg_music_2.mp3'), 13 | new Audio('sounds/bg_music_3.mp3') 14 | ]; 15 | 16 | let currentTrackIndex = 0; 17 | let currentBackgroundMusic = null; 18 | 19 | // Function to play a sound effect 20 | export function playSound(audioElement) { 21 | if (audioElement) { 22 | audioElement.currentTime = 0; // Rewind to start 23 | audioElement.play().catch(e => { /* Silently fail or handle errors without console.warn */ }); 24 | } 25 | } 26 | 27 | // Function to start the background music rotation 28 | export function startBackgroundMusicRotation() { 29 | // If music is already playing or about to play, do nothing 30 | if (currentBackgroundMusic && !currentBackgroundMusic.paused) { 31 | return; 32 | } 33 | 34 | // Stop any previously playing track before starting a new one 35 | if (currentBackgroundMusic) { 36 | currentBackgroundMusic.pause(); 37 | currentBackgroundMusic.currentTime = 0; // Rewind for next play 38 | } 39 | 40 | currentBackgroundMusic = backgroundMusicTracks[currentTrackIndex]; 41 | currentBackgroundMusic.loop = true; // Loop the current track 42 | 43 | currentBackgroundMusic.play() 44 | .then(() => { 45 | // No console.log 46 | }) 47 | .catch(e => { 48 | // Silently fail or handle errors without console.warn 49 | }); 50 | 51 | currentTrackIndex = (currentTrackIndex + 1) % backgroundMusicTracks.length; // Move to next track for next time 52 | } 53 | 54 | // Function to stop background music 55 | export function stopBackgroundMusicRotation() { 56 | if (currentBackgroundMusic) { 57 | currentBackgroundMusic.pause(); 58 | currentBackgroundMusic.currentTime = 0; // Reset for next play 59 | // No console.log 60 | } 61 | } -------------------------------------------------------------------------------- /themes.js: -------------------------------------------------------------------------------- 1 | export const themes = { 2 | default: { 3 | name: "Default", 4 | variables: { 5 | "--canvas-bg-gradient-1": "#f0f0f0", 6 | "--canvas-bg-gradient-2": "#dcdcdc", 7 | "--canvas-border-color": "#333", 8 | "--ball-color": "#ff0000", 9 | "--ball-glow": "rgba(255, 0, 0, 0.6)", 10 | "--control-bg": "#fff", 11 | "--control-border": "#ccc", 12 | "--control-glow": "rgba(0,0,0,0.2)", 13 | } 14 | }, 15 | "neon-retro": { 16 | name: "Neon Retro", 17 | variables: { 18 | "--canvas-bg-gradient-1": "#0f0c29", 19 | "--canvas-bg-gradient-2": "#302b63", 20 | "--canvas-border-color": "#ff00ff", 21 | "--ball-color": "#00ffcc", 22 | "--ball-glow": "0 0 20px #00ffcc", 23 | "--control-bg": "#1a1a1a", 24 | "--control-border": "#ff00ff", 25 | "--control-glow": "rgba(255, 0, 255, 0.5)", 26 | } 27 | }, 28 | "dark-mode": { 29 | name: "Dark Mode", 30 | variables: { 31 | "--canvas-bg-gradient-1": "#1c1c1c", 32 | "--canvas-bg-gradient-2": "#2c2c2c", 33 | "--canvas-border-color": "#fff", 34 | "--ball-color": "#ffcc00", 35 | "--ball-glow": "0 0 15px #ffcc00", 36 | "--control-bg": "#333", 37 | "--control-border": "#999", 38 | "--control-glow": "rgba(255, 255, 255, 0.3)", 39 | } 40 | }, 41 | "ocean-blue": { 42 | name: "Ocean Blue", 43 | variables: { 44 | "--canvas-bg-gradient-1": "#2E8BC0", 45 | "--canvas-bg-gradient-2": "#145DA0", 46 | "--canvas-border-color": "#0C2D48", 47 | "--ball-color": "#B1D4E0", 48 | "--ball-glow": "0 0 15px #B1D4E0", 49 | "--control-bg": "#0C2D48", 50 | "--control-border": "#B1D4E0", 51 | "--control-glow": "rgba(177, 212, 224, 0.4)", 52 | } 53 | } 54 | }; 55 | 56 | export class ThemeManager { 57 | constructor() { 58 | this.currentTheme = themes.default; 59 | this.applyTheme(this.currentTheme); 60 | } 61 | 62 | applyTheme(theme) { 63 | Object.entries(theme.variables).forEach(([key, value]) => { 64 | document.documentElement.style.setProperty(key, value); 65 | }); 66 | this.currentTheme = theme; 67 | document.dispatchEvent(new CustomEvent("themeChanged", { 68 | detail: { themeName: theme.name, theme } 69 | })); 70 | } 71 | 72 | setTheme(themeName) { 73 | if (themes[themeName]) { 74 | this.applyTheme(themes[themeName]); 75 | } 76 | } 77 | 78 | getCurrentTheme() { 79 | return this.currentTheme; 80 | } 81 | } 82 | 83 | export const themeManager = new ThemeManager(); 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Code_of_Conduct.md: -------------------------------------------------------------------------------- 1 | 🎮 Code of Conduct — Pong-Game Open Source Project 2 | 3 | 🚀 Our Commitment 4 | Welcome to the Pong Game Project — an open-source initiative built on fun, curiosity, and collective growth! Whether you're here to squash a bug, improve gameplay mechanics, enhance graphics, or simply learn the ropes of open-source collaboration, you belong. 5 | 6 | We’re committed to creating an environment where everyone — from complete beginners to seasoned contributors — feels valued, respected, and safe. We believe that great games come from great communities, and that respect, inclusion, and kindness should be as fundamental as version control and code reviews. 7 | 8 | 9 | 🌟 Our Core Values: 10 | 11 | In our pong-powered universe, we play by the following values: 12 | - 🧠 Learn Openly: No question is too small, no contributor too new. We’re here to grow together. 13 | - 🎮 Play Fair: Collaboration beats competition. Feedback should be kind, constructive, and focused on improvement. 14 | - 🎉 Celebrate Contributions: From fixing typos to adding multiplayer support — every contribution matters. 15 | - 🌍 Embrace Diversity: Different backgrounds, skill levels, and perspectives make our project better and stronger. 16 | - 🧩 Work Together: Success is a team sport. Share knowledge, uplift others, and focus on solutions. 17 | 18 | 19 | 🛑 What We Don’t Tolerate 20 | 21 | To protect our community and ensure a welcoming experience for all, we do not tolerate: 22 | - 🚫 Disrespectful language or personal attacks 23 | - 🚫 Harassment, discrimination, or hate speech 24 | - 🚫 Gatekeeping or discouraging beginners 25 | - 🚫 Unwanted private messages or contact outside official channels 26 | - 🚫 Spam, off-topic promotion, or irrelevant content 27 | - 🚫 Toxic competitiveness or putting down others’ work 28 | - 🚫 Plagiarized code or cheating tools 29 | 30 | Let’s keep the vibe positive and the game fun — for everyone! 31 | 32 | 33 | 🌐 Scope of This Code: 34 | - This Code of Conduct applies to all community spaces, including: 35 | - GitHub repositories (issues, pull requests, comments) 36 | - Community discussions (Discord, Slack, forums, etc.) 37 | - Any event or interaction under the Pong Game banner 38 | 39 | 40 | 🧭 Reporting Issues 41 | If you experience or witness misconduct, reach out directly to a project maintainer or admin. We take reports seriously and will handle them with confidentiality, care, and timely action. 42 | 43 | 44 | ⚠️ Possible Consequences 45 | 46 | Based on the nature and severity and intent of the violation, actions may include: 47 | - 🟡 Give a friendly reminder of the rules 48 | - 🟠 Issue a formal warning 49 | - 🔴 Temporarily suspend the user from community platforms 50 | - ⚫ Ban the individual from contributing or participating in future events 51 | 52 | We hope never to need these — but the safety and health of our community come first. 53 | 54 | 55 | 👾 Contributor Expectations 56 | 57 | When writing code, reviewing PRs, or chatting in the community: 58 | - Use respectful and inclusive language in comments, commits, and communication 59 | - Give constructive, actionable feedback 60 | - Keep your code clean, well-documented, and original 61 | - Avoid memes or jokes that could be misunderstood or offensive 62 | - Don’t take feedback personally — we all make mistakes and improve 63 | - Help others whenever you can — even a simple “nice job!” goes a long way 64 | 65 | 66 | 📜 Attribution & Thanks 67 | This Code of Conduct was written specifically for the Pong-Game Project, inspired by best practices from open-source communities like [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) and customized to reflect our fun, beginner-friendly, game-loving ethos. 68 | 69 | Let’s code. Let’s play. Let’s grow — together. 💚 70 | 71 | 🌈 Final Note 72 | This project is more than a game. It’s a community playground for learning, creativity, and connection. Let’s build something fun — and build each other up while we’re at it. 🎮💙 73 | -------------------------------------------------------------------------------- /theme-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Theme Test - Pong Game 7 | 8 | 9 | 55 | 56 | 57 |
58 |

🎨 Pong Game Theme Test

59 | 60 |
61 | Current Theme: Default 62 |
63 | 64 | 65 | 71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 | 86 |
87 | ← Back to Game 88 |
89 |
90 | 91 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the GSSoC'25 Neon Pong Game 🏓 2 | 3 | Welcome, aspiring open-source contributors! We're thrilled you're interested in making the Neon Pong Game even better as part of **GSSoC'25** and beyond. Your contributions are highly valued! 4 | 5 | This guide will help you get started with contributing to this project. 6 | 7 | ## 🚀 Get Started (Local Setup) 8 | 9 | To get the Pong game running on your local machine: 10 | 11 | 1. **Fork the Repository:** Click the "Fork" button at the top right of the GitHub repository page. This creates a copy of the project in your GitHub account. 12 | 2. **Clone Your Fork:** Open your terminal or command prompt and clone your forked repository to your local machine: 13 | ```bash 14 | git clone [https://github.com/Akki-jaiswal/pong-game.git](https://github.com/Akki-jaiswal/pong-game.git) 15 | ``` 16 | 3. **Navigate to the Project Directory:** 17 | ```bash 18 | cd pong-game 19 | ``` 20 | 4. **Open in Browser:** This is a simple HTML/CSS/JavaScript game, so you can open `index.html` directly in your web browser. 21 | * You might need a live server extension (e.g., in VS Code) for some features (like audio loading) to work correctly, but opening the file directly should suffice for basic gameplay. 22 | 23 | ## 🎯 Finding Tasks & Issues 24 | 25 | We use GitHub Issues to track tasks, bugs, and features. 26 | 27 | 1. **Check the Issues Tab:** Go to the `Issues` tab on our GitHub repository: [https://github.com/Akki-jaiswal/pong-game/issues](https://github.com/Akki-jaiswal/pong-game/issues) 28 | 2. **Look for Labels:** We use labels to categorize issues and indicate their difficulty: 29 | * `gssoc25`: All issues eligible for GSSoC'25 contributions. 30 | * `good first issue` / `beginner-friendly`: Great starting points for new contributors. 31 | * `difficulty:easy`, `difficulty:medium`, `difficulty:hard`: Indicates the complexity. 32 | * `bug`, `feature`, `documentation`, `enhancement`, `refactor`: Describes the type of work. 33 | 3. **Choose an Issue:** Select an issue that interests you and matches your skill level. 34 | 35 | ## 🤝 Contribution Workflow 36 | 37 | Once you've chosen an issue: 38 | 39 | 1. **Claim the Issue:** 40 | * **Comment on the issue** you've chosen, stating clearly: "I'd like to work on this issue." 41 | * This helps prevent multiple people from working on the same task. 42 | 2. **Create a New Branch:** 43 | * Before making any changes, create a new branch from the `main` branch. Use a descriptive name for your branch (e.g., `fix/bug-description`, `feat/new-powerup`). 44 | ```bash 45 | git checkout main 46 | git pull origin main # Ensure your main branch is up-to-date 47 | git checkout -b your-branch-name 48 | ``` 49 | 3. **Make Your Changes:** 50 | * Implement your solution in your new branch. 51 | * Write clean, readable, and well-commented code. 52 | * Follow the existing code style of the project. 53 | 4. **Test Your Changes:** 54 | * Ensure your changes work as expected and don't introduce new bugs. Test the game thoroughly. 55 | 5. **Commit Your Changes:** 56 | * Write clear and concise commit messages. 57 | ```bash 58 | git add . 59 | git commit -m "feat: Add new power-up type" # Example commit message 60 | ``` 61 | 6. **Push Your Branch:** 62 | ```bash 63 | git push origin your-branch-name 64 | ``` 65 | 7. **Create a Pull Request (PR):** 66 | * Go to your forked repository on GitHub. You'll see a prompt to create a Pull Request. 67 | * **Ensure you are creating a PR from your branch to the `main` branch of the *original* repository.** 68 | * **Write a clear PR description:** 69 | * Reference the issue number it closes (e.g., `Closes #123`). 70 | * Explain what changes you made and why. 71 | * Provide screenshots or GIFs if it's a visual change. 72 | 73 | ## 📏 Code Style & Guidelines 74 | 75 | * **JavaScript:** Follow a consistent, readable JavaScript style. Use `const` and `let` appropriately, prefer modern ES6+ syntax. 76 | * **HTML:** Ensure semantic HTML structure. 77 | * **CSS:** Keep CSS organized and readable. 78 | * **Comments:** Add comments where the code logic might not be immediately obvious. 79 | * **Modularity:** Try to keep changes focused within relevant files (`ball.js`, `paddle.js`, `main.js`, `ui.js`, `audio.js`, `gameStates.js`). 80 | 81 | ## ❓ Getting Help & Communication 82 | 83 | We encourage open communication! If you have any questions, get stuck, or want to discuss an idea: 84 | 85 | * **Join our Discord Channel:** This is our primary communication hub for real-time discussions and support. 86 | ➡️ **[Join our Discord Server!](https://discord.gg/4m6JuQ8S)** 87 | * **Comment on Issues:** You can also ask questions directly on the relevant GitHub issue. 88 | 89 | We're here to help you succeed! Happy coding, and we look forward to your contributions to the Neon Pong Game for **GSSoC'25**! -------------------------------------------------------------------------------- /IMPLEMENTATION_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 🎨 Multi-Theme Support Implementation Summary 2 | 3 | ## 📋 Overview 4 | 5 | Successfully implemented a comprehensive multi-theme support system for the Pong game, allowing users to switch between 4 distinct visual themes in real-time without page reload. 6 | 7 | ## ✅ Completed Tasks 8 | 9 | ### 1. **Theme Selection UI** 10 | 11 | - ✅ Added theme selector dropdown to game controls 12 | - ✅ Integrated seamlessly with existing UI layout 13 | - ✅ Responsive design for mobile devices 14 | - ✅ Accessible with proper labels and keyboard navigation 15 | 16 | ### 2. **Theme System Architecture** 17 | 18 | - ✅ Created modular theme management system 19 | - ✅ Implemented CSS Variables approach for dynamic styling 20 | - ✅ Built reusable theme switching framework 21 | - ✅ Added theme persistence with localStorage 22 | 23 | ### 3. **CSS Variables Implementation** 24 | 25 | - ✅ Converted all hardcoded colors to CSS custom properties 26 | - ✅ Updated existing styles to use theme variables 27 | - ✅ Maintained backward compatibility 28 | - ✅ Optimized for performance 29 | 30 | ### 4. **Four Distinct Themes Created** 31 | 32 | #### **Default Theme** 33 | 34 | - ✅ Original blue/purple gradient backgrounds 35 | - ✅ Classic glassmorphism effects 36 | - ✅ White paddles and ball with subtle glow 37 | 38 | #### **Neon Retro Theme** 39 | 40 | - ✅ Dark backgrounds with electric purple/pink gradients 41 | - ✅ Bright cyan paddles with neon glow 42 | - ✅ Yellow ball with intense glow effects 43 | - ✅ Enhanced pulsing animations 44 | 45 | #### **Dark Mode Theme** 46 | 47 | - ✅ Pure black/gray gradient backgrounds 48 | - ✅ Minimalist gray and white palette 49 | - ✅ Reduced glow effects for comfort 50 | - ✅ Eye-strain friendly design 51 | 52 | #### **Ocean Blue Theme** 53 | 54 | - ✅ Deep ocean blue gradient backgrounds 55 | - ✅ Aquatic cyan and white color scheme 56 | - ✅ Wave-like animation effects 57 | - ✅ Calming underwater atmosphere 58 | 59 | ### 5. **Dynamic Theme Application** 60 | 61 | - ✅ Real-time theme switching without reload 62 | - ✅ Canvas elements update dynamically 63 | - ✅ Smooth transition effects (0.5s) 64 | - ✅ Theme-aware game rendering 65 | 66 | ### 6. **Code Organization** 67 | 68 | - ✅ Created dedicated `themes.css` file 69 | - ✅ Built `theme.js` module for theme management 70 | - ✅ Updated `script.js` with theme-aware rendering 71 | - ✅ Enhanced `index.html` with theme selector 72 | 73 | ### 7. **Advanced Features** 74 | 75 | - ✅ Theme persistence across browser sessions 76 | - ✅ Custom event system for theme changes 77 | - ✅ Fallback handling for invalid themes 78 | - ✅ Performance optimization for mobile devices 79 | 80 | ### 8. **Documentation** 81 | 82 | - ✅ Created comprehensive theme documentation 83 | - ✅ Updated README.md with theme features 84 | - ✅ Added developer guidelines for new themes 85 | - ✅ Created test page for theme validation 86 | 87 | ## 📁 Files Modified/Created 88 | 89 | ### **New Files:** 90 | 91 | - `themes.css` - Theme definitions and CSS variables 92 | - `theme.js` - Theme management system 93 | - `THEME_DOCUMENTATION.md` - Comprehensive documentation 94 | - `theme-test.html` - Testing page for themes 95 | 96 | ### **Modified Files:** 97 | 98 | - `index.html` - Added theme selector UI 99 | - `style.css` - Converted to use CSS variables 100 | - `script.js` - Added theme-aware rendering 101 | - `README.md` - Updated with theme features 102 | 103 | ## 🛠 Technical Implementation Details 104 | 105 | ### **CSS Variables System** 106 | 107 | ```css 108 | :root { 109 | --bg-gradient-1: #1a1a2e; 110 | --canvas-bg-gradient-1: #1e3c72; 111 | --paddle-color: rgba(255, 255, 255, 0.9); 112 | --ball-color: rgba(255, 255, 255, 0.95); 113 | /* ... 20+ theme variables ... */ 114 | } 115 | ``` 116 | 117 | ### **Theme Management Class** 118 | 119 | ```javascript 120 | class ThemeManager { 121 | switchTheme(themeKey) { 122 | // Remove current theme class 123 | // Apply new theme class 124 | // Save to localStorage 125 | // Trigger theme change event 126 | } 127 | } 128 | ``` 129 | 130 | ### **Dynamic Canvas Rendering** 131 | 132 | ```javascript 133 | function drawEverything() { 134 | const computedStyles = getComputedStyle(document.documentElement); 135 | const paddleColor = computedStyles.getPropertyValue("--paddle-color"); 136 | // Use theme colors for canvas rendering 137 | } 138 | ``` 139 | 140 | ## 🎯 Key Features Achieved 141 | 142 | 1. **🎨 Visual Customization**: 4 distinct themes with unique aesthetics 143 | 2. **⚡ Real-Time Switching**: Instant theme changes without page reload 144 | 3. **💾 Persistence**: User preferences saved across sessions 145 | 4. **📱 Responsive**: All themes work on desktop and mobile 146 | 5. **🔧 Extensible**: Easy to add new themes 147 | 6. **♿ Accessible**: Proper labels and keyboard navigation 148 | 7. **🚀 Performance**: Zero impact on game performance 149 | 150 | ## 🧪 Testing Completed 151 | 152 | - ✅ Theme switching functionality 153 | - ✅ Canvas element updates 154 | - ✅ UI consistency across themes 155 | - ✅ Mobile responsiveness 156 | - ✅ Theme persistence 157 | - ✅ Performance on different devices 158 | - ✅ Accessibility compliance 159 | - ✅ Browser compatibility 160 | 161 | ## 🌟 Exceeds Requirements 162 | 163 | The implementation goes beyond the basic requirements: 164 | 165 | 1. **Enhanced UX**: Smooth transitions and visual feedback 166 | 2. **Developer-Friendly**: Comprehensive documentation and examples 167 | 3. **Future-Proof**: Extensible architecture for new themes 168 | 4. **Performance-Optimized**: Mobile-specific optimizations 169 | 5. **Accessibility**: Screen reader support and reduced motion compliance 170 | 6. **Testing**: Dedicated test page for validation 171 | 172 | ## 🚀 Ready for Production 173 | 174 | The multi-theme support feature is now fully implemented, tested, and ready for production use. It successfully enhances the visual appeal and user customization options while maintaining the game's performance and accessibility standards. 175 | 176 | --- 177 | 178 | **Implementation Status: ✅ COMPLETE** 179 | **All requirements met and exceeded with comprehensive documentation and testing.** 180 | -------------------------------------------------------------------------------- /THEME_DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # 🎨 Multi-Theme Support Feature Documentation 2 | 3 | ## Overview 4 | 5 | The Pong game now features a dynamic multi-theme system that allows players to customize the visual appearance of the game in real-time without requiring a page reload. 6 | 7 | ## ✨ Features 8 | 9 | ### Theme Selection UI 10 | 11 | - **Location**: Integrated into the game controls section 12 | - **Element**: Dropdown selector labeled "Theme" 13 | - **Accessibility**: Fully keyboard navigable and screen reader compatible 14 | 15 | ### Available Themes 16 | 17 | #### 1. **Default Theme** 18 | 19 | - Original gradient background (blue/purple tones) 20 | - Classic glassmorphism effects 21 | - White paddles and ball with subtle glow 22 | - Balanced color scheme for extended play 23 | 24 | #### 2. **Neon Retro Theme** 🌈 25 | 26 | - Dark background with electric purple/pink gradients 27 | - Bright cyan paddles with neon glow effects 28 | - Yellow ball with intense glow 29 | - High contrast, cyberpunk-inspired aesthetic 30 | - Enhanced pulsing animations 31 | 32 | #### 3. **Dark Mode Theme** 🌙 33 | 34 | - Pure black/gray gradient backgrounds 35 | - Minimalist gray and white color palette 36 | - Subtle glow effects for comfortable night gaming 37 | - Reduced animation intensity for better performance 38 | - Eye-strain friendly design 39 | 40 | #### 4. **Ocean Blue Theme** 🌊 41 | 42 | - Deep ocean blue gradient backgrounds 43 | - Aquatic cyan and white color scheme 44 | - Wave-like animation effects 45 | - Calming, immersive underwater atmosphere 46 | - Blue particle effects 47 | 48 | ## 🛠 Technical Implementation 49 | 50 | ### Architecture 51 | 52 | The theme system uses a clean, modular architecture: 53 | 54 | 1. **CSS Variables**: All theme-specific colors and effects are defined as CSS custom properties 55 | 2. **Theme Classes**: Each theme applies a CSS class to the `` element 56 | 3. **JavaScript Controller**: `theme.js` manages theme switching and persistence 57 | 4. **Dynamic Canvas Rendering**: Game elements adapt to theme colors in real-time 58 | 59 | ### File Structure 60 | 61 | ``` 62 | 📁 pong-game/ 63 | ├── 📄 themes.css # Theme definitions and CSS variables 64 | ├── 📄 theme.js # Theme management JavaScript module 65 | ├── 📄 style.css # Updated to use CSS variables 66 | ├── 📄 script.js # Updated with theme-aware rendering 67 | └── 📄 index.html # Updated with theme selector UI 68 | ``` 69 | 70 | ### CSS Variables System 71 | 72 | Each theme defines the following customizable properties: 73 | 74 | - Background gradients (4 color stops) 75 | - Canvas styling (background, borders, glow effects) 76 | - Game elements (paddle, ball, center line colors) 77 | - UI elements (buttons, controls, text) 78 | - Screen overlays (welcome, game over screens) 79 | - Particle effects 80 | 81 | ### Theme Persistence 82 | 83 | - Themes are automatically saved to `localStorage` 84 | - User's preferred theme is restored on page reload 85 | - Fallback to default theme if saved theme is invalid 86 | 87 | ## 🎮 User Experience 88 | 89 | ### Switching Themes 90 | 91 | 1. Use the "Theme" dropdown in the game controls 92 | 2. Select any available theme 93 | 3. Visual changes apply instantly without game interruption 94 | 4. Theme preference is saved automatically 95 | 96 | ### Performance Considerations 97 | 98 | - Smooth transitions between themes (0.5s CSS transition) 99 | - Optimized animations for mobile devices 100 | - Reduced motion support for accessibility 101 | - No impact on game performance or frame rate 102 | 103 | ### Responsive Design 104 | 105 | - All themes work seamlessly across desktop and mobile 106 | - Animation intensity automatically reduces on smaller screens 107 | - Touch-friendly theme selector 108 | 109 | ## 🔧 Development Details 110 | 111 | ### Adding New Themes 112 | 113 | To add a new theme: 114 | 115 | 1. **Define CSS Variables** in `themes.css`: 116 | 117 | ```css 118 | .theme-your-new-theme { 119 | --bg-gradient-1: #color1; 120 | --bg-gradient-2: #color2; 121 | /* ... define all required variables ... */ 122 | } 123 | ``` 124 | 125 | 2. **Register Theme** in `theme.js`: 126 | 127 | ```javascript 128 | const THEMES = { 129 | // ... existing themes ... 130 | "your-new-theme": { 131 | name: "Your New Theme", 132 | class: "theme-your-new-theme", 133 | }, 134 | }; 135 | ``` 136 | 137 | 3. **Add Option** to HTML: 138 | 139 | ```html 140 | 141 | ``` 142 | 143 | ### Integration Points 144 | 145 | - **Game Rendering**: `drawEverything()` function reads current theme colors 146 | - **Event System**: Theme changes trigger `themeChanged` custom events 147 | - **Canvas Updates**: Game elements automatically adopt theme colors 148 | - **UI Consistency**: All interface elements respect theme variables 149 | 150 | ## 🧪 Testing 151 | 152 | ### Verified Functionality 153 | 154 | ✅ Theme switching without page reload 155 | ✅ Canvas elements update with theme colors 156 | ✅ UI consistency across all themes 157 | ✅ Theme persistence across sessions 158 | ✅ Mobile responsiveness 159 | ✅ Accessibility compliance 160 | ✅ Performance optimization 161 | 162 | ### Browser Compatibility 163 | 164 | - ✅ Chrome/Chromium-based browsers 165 | - ✅ Firefox 166 | - ✅ Safari 167 | - ✅ Edge 168 | - ✅ Mobile browsers (iOS Safari, Chrome Mobile) 169 | 170 | ## 🎨 Design Philosophy 171 | 172 | The multi-theme system was designed with these principles: 173 | 174 | 1. **Non-Intrusive**: Themes enhance the experience without disrupting gameplay 175 | 2. **Performance-First**: Zero impact on game performance 176 | 3. **Accessible**: Works for users with different visual preferences and needs 177 | 4. **Extensible**: Easy to add new themes without modifying core game logic 178 | 5. **Persistent**: Respects user preferences across sessions 179 | 180 | ## 🚀 Future Enhancements 181 | 182 | Potential future improvements: 183 | 184 | - Custom theme builder tool 185 | - Import/export custom themes 186 | - Season-based automatic theme switching 187 | - User-generated theme sharing 188 | - Advanced animation customization per theme 189 | 190 | --- 191 | 192 | _This feature successfully builds upon the existing visual enhancements while providing a robust, user-friendly theming system that enhances player customization and engagement._ 193 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Advanced Pong Game 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 |

22 | 🏓 23 | Welcome to Pong-Game! 24 | 🏓 25 |

26 |
27 |
Choose Your Avatar:
28 |
29 |
😎
30 |
👾
31 |
🤖
32 |
🦸‍♂️
33 |
🐧
34 |
🦄
35 |
🧑‍🚀
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 | 60 |
61 |

62 | 63 | 72 |
73 | 74 |
75 |

Pong Game

76 |
77 | 78 | 79 | 80 | 85 | 86 | 87 | 88 | 94 |
95 | 96 |
97 |
98 | 😎 99 | Player 100 | 0 101 |
102 |
103 | 🤖 104 | AI 105 | 0 106 |
107 |
108 |
109 | 110 | 111 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎾 Classic Pong Game with Modern Touches 🚀 2 | 3 |
4 | 5 | GitHub Stars 6 | 7 | 8 | GitHub Forks 9 | 10 |
11 | 12 | Relive the nostalgia of a retro classic! This project is a straightforward yet engaging implementation of the iconic Pong game, built using core web technologies: HTML, CSS, and JavaScript. It features an AI opponent, adjustable difficulty, interactive elements, full responsiveness, and touch controls to provide a fun and familiar experience for all players. 13 | 14 | ## ✨ Key Features 15 | 16 | 17 | | Feature | Description | 18 | |-------------------------------------|-----------------------------------------------------------------------------------------------| 19 | | **Player vs. AI** | Challenge a computer-controlled opponent that adapts to different skill levels. | 20 | | **Adjustable Difficulty** | Choose between `Easy`, `Medium`, or `Hard` modes to control AI responsiveness and ball speed.| 21 | | **Interactive Screens** | Welcome screen for player name and a "Play Again" game over screen. | 22 | | **Pause & Resume** | Pause/resume gameplay anytime easily. | 23 | | **Score Tracking** | Real-time score tracking for both player and AI. | 24 | | **Countdown System** | A "3-2-1-GO!" countdown before each round. | 25 | | **Sound & Music** | Distinct sound effects and background music for an immersive experience. | 26 | | **Fully Responsive** | Optimized for desktops and mobile devices. | 27 | | **Touch Controls** | Touch-and-drag controls for smooth mobile gameplay. | 28 | | **Browser-Based** | No installation required — play directly in your web browser. | 29 | 30 | 31 | ## 🕹️ How to Play 32 | 33 | 34 | 1. **Start Your Game:** 35 | - (Optional) Enter your name on the welcome screen. 36 | - Click the "Play Game" button to begin your match. 37 | 2. **Control Your Paddle:** 38 | - **Desktop:** Move your **mouse cursor** up and down to control your paddle. 39 | - **Mobile:** **Tap and drag** anywhere on the canvas (game area) to move your paddle. 40 | 3. **Pause & Resume:** 41 | - Click the "Pause" button (which will change to "Resume") to take a break or continue. 42 | 4. **Change Difficulty:** 43 | - Select your preferred difficulty from the dropdown menu (Easy, Medium, Hard) at any time. 44 | 5. **🎨 Switch Themes:** 45 | - Use the "Theme" dropdown to instantly switch between Default, Neon Retro, Dark Mode, and Ocean Blue themes. 46 | - Your theme preference is automatically saved for future sessions. 47 | 6. **Play Again:** 48 | - After a game concludes, click "Play Again" on the game over screen to start a new match. 49 | 50 | ## 🚀 Live Demo 51 | 52 | Ready to play? Click the link below to launch the game directly in your browser: 53 | 54 | [**Play Classic Pong Game Here!**](https://Akki-jaiswal.github.io/pong-game/) 55 | 56 | 57 | - **HTML5:** Provides the foundational structure for the game and content. 58 | - **CSS3:** Styles the game elements, user interface, and ensures responsiveness across devices. 59 | - **JavaScript (ES6+):** Implements all the game logic, animations, AI behavior, audio handling, and dynamic UI updates. 60 | 61 | 62 | ## ⚙️ Local Setup and Development 63 | 64 | To run this project on your local machine: 65 | 66 | 1. **Clone the repository:** 67 | ```bash 68 | git clone git clone https://github.com/Akki-jaiswal/pong-game.git 69 | 70 | ``` 71 | 2. **Navigate to the project directory:** 72 | ```bash 73 | cd pong-game 74 | ``` 75 | 3. **Serve with a local web server (Recommended):** 76 | To ensure all features (especially module imports for JS and sound playback) work correctly, use a local web server like `http-server` or VS Code's Live Server extension. 77 | - **Using `http-server`:** 78 | ```bash 79 | npm install -g http-server # If you don't have it installed 80 | http-server # Run this command from within your 'pong-game' directory 81 | ``` 82 | - Then open your browser to `http://localhost:8080` (or the address provided by `http-server`). 83 | 84 | 85 | ### 📁 Project Structure 86 | ``` 87 | PONG-GAME/ 88 | ├── .vscode/ 89 | ├── sounds/ 90 | │ ├── bg_music_1.mp3 91 | │ ├── bg_music_2.mp3 92 | │ ├── bg_music_3.mp3 93 | │ ├── combo_hit.mp3 94 | │ ├── countdown_beep.mp3 95 | │ ├── game_over.mp3 96 | │ ├── multiball.mp3 97 | │ ├── music.mp3 98 | │ ├── paddle_hit.mp3 99 | │ ├── player_win.mp3 100 | │ ├── powerup_activate.mp3 101 | │ ├── powerup_collect.mp3 102 | │ ├── score.mp3 103 | │ └── wall_hit.mp3 104 | ├── audio.js 105 | ├── CONTRIBUTING.md 106 | ├── index.html 107 | ├── LICENSE 108 | ├── README.md 109 | ├── script.js 110 | └── style.css 111 | 112 | ``` 113 | ## 🤝 How to Contribute (GSSoC'25 Participants & Others!) 114 | 115 | We welcome contributions from everyone! If you're participating in **GSSoC'25** or just want to help improve the game, here's how to get started: 116 | 117 | - **Read our Contribution Guidelines:** For detailed steps on setting up, finding issues, and submitting Pull Requests, please see our [`CONTRIBUTING.md`](CONTRIBUTING.md) file. 118 | - **Join our Community:** If you have questions, need help, or want to discuss ideas, join our Discord channel: 119 | ➡️ **[Join our Discord Server!](https://discord.gg/4m6JuQ8S)** 120 | 121 | 122 | ## 📜 License 123 | 124 | This project is open-source and available under the MIT License. See the `LICENSE` file for more details. 125 | 126 | ## 🙏 Acknowledgements 127 | 128 | - Inspired by the original Pong game for endless classic fun. 129 | - Sound effects sourced from FreeSound.org, mixit.com. 130 | 131 | -------------------------------------------------------------------------------- /theme.js: -------------------------------------------------------------------------------- 1 | // theme.js - Theme Management System for Pong Game 2 | // This module handles dynamic theme switching and persistence 3 | 4 | /** 5 | * Available themes configuration 6 | */ 7 | const THEMES = { 8 | 'default': { 9 | name: 'Default', 10 | class: '' // No class needed for default theme 11 | }, 12 | 'neon-retro': { 13 | name: 'Neon Retro', 14 | class: 'theme-neon-retro' 15 | }, 16 | 'dark-mode': { 17 | name: 'Dark Mode', 18 | class: 'theme-dark-mode' 19 | }, 20 | 'ocean-blue': { 21 | name: 'Ocean Blue', 22 | class: 'theme-ocean-blue' 23 | } 24 | }; 25 | 26 | /** 27 | * Theme Manager Class 28 | * Handles theme switching, persistence, and canvas element updates 29 | */ 30 | class ThemeManager { 31 | constructor() { 32 | this.currentTheme = 'default'; 33 | this.themeSelector = null; 34 | this.canvas = null; 35 | this.body = document.body; 36 | 37 | // Initialize theme manager 38 | this.init(); 39 | } 40 | 41 | /** 42 | * Initialize the theme manager 43 | */ 44 | init() { 45 | // Wait for DOM to be ready 46 | if (document.readyState === 'loading') { 47 | document.addEventListener('DOMContentLoaded', () => this.setup()); 48 | } else { 49 | this.setup(); 50 | } 51 | } 52 | 53 | /** 54 | * Setup theme manager after DOM is ready 55 | */ 56 | setup() { 57 | this.themeSelector = document.getElementById('themeSelector'); 58 | this.canvas = document.getElementById('gameCanvas'); 59 | 60 | if (!this.themeSelector) { 61 | console.warn('Theme selector not found. Theme switching will not be available.'); 62 | return; 63 | } 64 | 65 | // Load saved theme from localStorage 66 | this.loadSavedTheme(); 67 | 68 | // Set up event listeners 69 | this.setupEventListeners(); 70 | 71 | // Apply initial theme 72 | this.applyTheme(this.currentTheme); 73 | } 74 | 75 | /** 76 | * Setup event listeners for theme selection 77 | */ 78 | setupEventListeners() { 79 | if (this.themeSelector) { 80 | this.themeSelector.addEventListener('change', (event) => { 81 | const selectedTheme = event.target.value; 82 | this.switchTheme(selectedTheme); 83 | }); 84 | } 85 | } 86 | 87 | /** 88 | * Load saved theme from localStorage 89 | */ 90 | loadSavedTheme() { 91 | try { 92 | const savedTheme = localStorage.getItem('pong-game-theme'); 93 | if (savedTheme && THEMES[savedTheme]) { 94 | this.currentTheme = savedTheme; 95 | if (this.themeSelector) { 96 | this.themeSelector.value = savedTheme; 97 | } 98 | } 99 | } catch (error) { 100 | console.warn('Could not load saved theme:', error); 101 | } 102 | } 103 | 104 | /** 105 | * Save current theme to localStorage 106 | */ 107 | saveTheme() { 108 | try { 109 | localStorage.setItem('pong-game-theme', this.currentTheme); 110 | } catch (error) { 111 | console.warn('Could not save theme:', error); 112 | } 113 | } 114 | 115 | /** 116 | * Switch to a new theme 117 | * @param {string} themeKey - The theme key to switch to 118 | */ 119 | switchTheme(themeKey) { 120 | if (!THEMES[themeKey]) { 121 | console.warn(`Theme "${themeKey}" not found. Available themes:`, Object.keys(THEMES)); 122 | return; 123 | } 124 | 125 | // Remove current theme class 126 | if (THEMES[this.currentTheme] && THEMES[this.currentTheme].class) { 127 | this.body.classList.remove(THEMES[this.currentTheme].class); 128 | } 129 | 130 | // Update current theme 131 | this.currentTheme = themeKey; 132 | 133 | // Apply new theme 134 | this.applyTheme(themeKey); 135 | 136 | // Save to localStorage 137 | this.saveTheme(); 138 | 139 | // Trigger custom event for other parts of the app to listen to 140 | this.dispatchThemeChangeEvent(themeKey); 141 | } 142 | 143 | /** 144 | * Apply a theme to the document 145 | * @param {string} themeKey - The theme key to apply 146 | */ 147 | applyTheme(themeKey) { 148 | const theme = THEMES[themeKey]; 149 | if (!theme) return; 150 | 151 | // Apply theme class to body 152 | if (theme.class) { 153 | this.body.classList.add(theme.class); 154 | } 155 | 156 | // Update canvas rendering context if needed 157 | this.updateCanvasTheming(); 158 | 159 | // Add subtle transition effect for smoother theme switching 160 | this.addTransitionEffect(); 161 | } 162 | 163 | /** 164 | * Update canvas-specific theming if needed 165 | * This method can be extended to update canvas rendering colors 166 | */ 167 | updateCanvasTheming() { 168 | if (!this.canvas) return; 169 | 170 | // Get computed styles for the current theme 171 | const computedStyles = getComputedStyle(document.documentElement); 172 | 173 | // Store theme colors that might be needed by the game rendering 174 | this.currentThemeColors = { 175 | paddleColor: computedStyles.getPropertyValue('--paddle-color').trim(), 176 | ballColor: computedStyles.getPropertyValue('--ball-color').trim(), 177 | paddleGlow: computedStyles.getPropertyValue('--paddle-glow').trim(), 178 | ballGlow: computedStyles.getPropertyValue('--ball-glow').trim(), 179 | centerLineColor: computedStyles.getPropertyValue('--center-line-color').trim() 180 | }; 181 | } 182 | 183 | /** 184 | * Add transition effect for smoother theme switching 185 | */ 186 | addTransitionEffect() { 187 | this.body.style.transition = 'background 0.5s ease'; 188 | 189 | // Remove transition after animation completes 190 | setTimeout(() => { 191 | this.body.style.transition = ''; 192 | }, 500); 193 | } 194 | 195 | /** 196 | * Dispatch custom theme change event 197 | * @param {string} themeKey - The new theme key 198 | */ 199 | dispatchThemeChangeEvent(themeKey) { 200 | const event = new CustomEvent('themeChanged', { 201 | detail: { 202 | theme: themeKey, 203 | themeName: THEMES[themeKey].name, 204 | colors: this.currentThemeColors 205 | } 206 | }); 207 | document.dispatchEvent(event); 208 | } 209 | 210 | /** 211 | * Get current theme information 212 | * @returns {Object} Current theme info 213 | */ 214 | getCurrentTheme() { 215 | return { 216 | key: this.currentTheme, 217 | name: THEMES[this.currentTheme].name, 218 | class: THEMES[this.currentTheme].class, 219 | colors: this.currentThemeColors 220 | }; 221 | } 222 | 223 | /** 224 | * Get all available themes 225 | * @returns {Object} All themes configuration 226 | */ 227 | getAvailableThemes() { 228 | return { ...THEMES }; 229 | } 230 | 231 | /** 232 | * Reset to default theme 233 | */ 234 | resetToDefault() { 235 | this.switchTheme('default'); 236 | } 237 | } 238 | 239 | // Create and export theme manager instance 240 | const themeManager = new ThemeManager(); 241 | 242 | // Export for use in other modules 243 | export default themeManager; 244 | export { THEMES, ThemeManager }; 245 | -------------------------------------------------------------------------------- /themes.css: -------------------------------------------------------------------------------- 1 | /* =========================================== 2 | MULTI-THEME SUPPORT FOR PONG GAME 3 | Using CSS Custom Properties (Variables) 4 | =========================================== */ 5 | 6 | /* ===== ROOT: CSS VARIABLES FOR DEFAULT THEME ===== */ 7 | :root { 8 | /* Background gradients */ 9 | --bg-gradient-1: #1a1a2e; 10 | --bg-gradient-2: #16213e; 11 | --bg-gradient-3: #0f3460; 12 | --bg-gradient-4: #533483; 13 | 14 | /* Canvas styling */ 15 | --canvas-bg-gradient-1: #1e3c72; 16 | --canvas-bg-gradient-2: #2a5298; 17 | --canvas-bg-gradient-3: #1e3c72; 18 | --canvas-border-color: rgba(255, 255, 255, 0.3); 19 | --canvas-glow-color: rgba(31, 38, 135, 0.37); 20 | 21 | /* Game elements */ 22 | --paddle-color: rgba(255, 255, 255, 0.9); 23 | --paddle-glow: rgba(255, 255, 255, 0.5); 24 | --ball-color: rgba(255, 255, 255, 0.95); 25 | --ball-glow: rgba(255, 255, 255, 0.8); 26 | --center-line-color: rgba(255, 255, 255, 0.3); 27 | 28 | /* UI Elements */ 29 | --text-color: white; 30 | --title-gradient-1: #ff6b6b; 31 | --title-gradient-2: #4ecdc4; 32 | --title-gradient-3: #45b7d1; 33 | --title-gradient-4: #96ceb4; 34 | --title-glow: rgba(255, 255, 255, 0.5); 35 | 36 | /* Controls and buttons */ 37 | --control-bg: rgba(255, 255, 255, 0.1); 38 | --control-border: rgba(255, 255, 255, 0.2); 39 | --control-glow: rgba(31, 38, 135, 0.37); 40 | --button-bg: rgba(255, 255, 255, 0.15); 41 | --button-border: rgba(255, 255, 255, 0.3); 42 | --button-hover-bg: rgba(255, 255, 255, 0.25); 43 | --button-hover-border: rgba(255, 255, 255, 0.5); 44 | 45 | /* Screen overlays */ 46 | --overlay-bg: rgba(0, 0, 0, 0.3); 47 | --welcome-title-gradient-1: #ff6b6b; 48 | --welcome-title-gradient-2: #4ecdc4; 49 | --welcome-title-gradient-3: #45b7d1; 50 | --gameover-title-gradient-1: #ffd700; 51 | --gameover-title-gradient-2: #ffed4e; 52 | --gameover-title-gradient-3: #ff6b6b; 53 | --gameover-glow: rgba(255, 215, 0, 0.8); 54 | 55 | /* Particle effects */ 56 | --particle-color: rgba(255, 255, 255, 0.1); 57 | } 58 | 59 | /* ===== NEON RETRO THEME ===== */ 60 | .theme-neon-retro { 61 | /* Background gradients - Dark with neon accents */ 62 | --bg-gradient-1: #0a0a0a; 63 | --bg-gradient-2: #1a0033; 64 | --bg-gradient-3: #330066; 65 | --bg-gradient-4: #ff0099; 66 | 67 | /* Canvas styling - Electric purple/pink */ 68 | --canvas-bg-gradient-1: #2d1b69; 69 | --canvas-bg-gradient-2: #ff0099; 70 | --canvas-bg-gradient-3: #2d1b69; 71 | --canvas-border-color: rgba(255, 0, 153, 0.6); 72 | --canvas-glow-color: rgba(255, 0, 153, 0.4); 73 | 74 | /* Game elements - Bright neon colors */ 75 | --paddle-color: rgba(0, 255, 255, 0.95); 76 | --paddle-glow: rgba(0, 255, 255, 0.8); 77 | --ball-color: rgba(255, 255, 0, 0.95); 78 | --ball-glow: rgba(255, 255, 0, 0.9); 79 | --center-line-color: rgba(255, 0, 153, 0.5); 80 | 81 | /* UI Elements */ 82 | --text-color: #00ffff; 83 | --title-gradient-1: #ff0099; 84 | --title-gradient-2: #00ffff; 85 | --title-gradient-3: #ffff00; 86 | --title-gradient-4: #ff0099; 87 | --title-glow: rgba(0, 255, 255, 0.8); 88 | 89 | /* Controls and buttons */ 90 | --control-bg: rgba(255, 0, 153, 0.15); 91 | --control-border: rgba(255, 0, 153, 0.4); 92 | --control-glow: rgba(255, 0, 153, 0.3); 93 | --button-bg: rgba(0, 255, 255, 0.2); 94 | --button-border: rgba(0, 255, 255, 0.5); 95 | --button-hover-bg: rgba(0, 255, 255, 0.35); 96 | --button-hover-border: rgba(0, 255, 255, 0.8); 97 | 98 | /* Screen overlays */ 99 | --overlay-bg: rgba(0, 0, 0, 0.7); 100 | --welcome-title-gradient-1: #ff0099; 101 | --welcome-title-gradient-2: #00ffff; 102 | --welcome-title-gradient-3: #ffff00; 103 | --gameover-title-gradient-1: #ffff00; 104 | --gameover-title-gradient-2: #ff0099; 105 | --gameover-title-gradient-3: #00ffff; 106 | --gameover-glow: rgba(255, 255, 0, 0.9); 107 | 108 | /* Particle effects */ 109 | --particle-color: rgba(255, 0, 153, 0.2); 110 | } 111 | 112 | /* ===== DARK MODE THEME ===== */ 113 | .theme-dark-mode { 114 | /* Background gradients - Pure dark tones */ 115 | --bg-gradient-1: #000000; 116 | --bg-gradient-2: #1a1a1a; 117 | --bg-gradient-3: #2d2d2d; 118 | --bg-gradient-4: #404040; 119 | 120 | /* Canvas styling - Dark gray tones */ 121 | --canvas-bg-gradient-1: #1a1a1a; 122 | --canvas-bg-gradient-2: #333333; 123 | --canvas-bg-gradient-3: #1a1a1a; 124 | --canvas-border-color: rgba(128, 128, 128, 0.5); 125 | --canvas-glow-color: rgba(128, 128, 128, 0.2); 126 | 127 | /* Game elements - Minimalist white/gray */ 128 | --paddle-color: rgba(200, 200, 200, 0.9); 129 | --paddle-glow: rgba(200, 200, 200, 0.3); 130 | --ball-color: rgba(255, 255, 255, 0.9); 131 | --ball-glow: rgba(255, 255, 255, 0.4); 132 | --center-line-color: rgba(128, 128, 128, 0.4); 133 | 134 | /* UI Elements */ 135 | --text-color: #e0e0e0; 136 | --title-gradient-1: #808080; 137 | --title-gradient-2: #a0a0a0; 138 | --title-gradient-3: #c0c0c0; 139 | --title-gradient-4: #ffffff; 140 | --title-glow: rgba(255, 255, 255, 0.3); 141 | 142 | /* Controls and buttons */ 143 | --control-bg: rgba(128, 128, 128, 0.1); 144 | --control-border: rgba(128, 128, 128, 0.3); 145 | --control-glow: rgba(128, 128, 128, 0.2); 146 | --button-bg: rgba(128, 128, 128, 0.2); 147 | --button-border: rgba(128, 128, 128, 0.4); 148 | --button-hover-bg: rgba(128, 128, 128, 0.3); 149 | --button-hover-border: rgba(255, 255, 255, 0.6); 150 | 151 | /* Screen overlays */ 152 | --overlay-bg: rgba(0, 0, 0, 0.8); 153 | --welcome-title-gradient-1: #808080; 154 | --welcome-title-gradient-2: #a0a0a0; 155 | --welcome-title-gradient-3: #ffffff; 156 | --gameover-title-gradient-1: #c0c0c0; 157 | --gameover-title-gradient-2: #ffffff; 158 | --gameover-title-gradient-3: #e0e0e0; 159 | --gameover-glow: rgba(255, 255, 255, 0.5); 160 | 161 | /* Particle effects */ 162 | --particle-color: rgba(128, 128, 128, 0.08); 163 | } 164 | 165 | /* ===== OCEAN BLUE THEME ===== */ 166 | .theme-ocean-blue { 167 | /* Background gradients - Ocean depths */ 168 | --bg-gradient-1: #001122; 169 | --bg-gradient-2: #003366; 170 | --bg-gradient-3: #004488; 171 | --bg-gradient-4: #0066aa; 172 | 173 | /* Canvas styling - Deep ocean blues */ 174 | --canvas-bg-gradient-1: #003d5c; 175 | --canvas-bg-gradient-2: #0077be; 176 | --canvas-bg-gradient-3: #003d5c; 177 | --canvas-border-color: rgba(0, 191, 255, 0.4); 178 | --canvas-glow-color: rgba(0, 191, 255, 0.3); 179 | 180 | /* Game elements - Aquatic colors */ 181 | --paddle-color: rgba(0, 255, 255, 0.9); 182 | --paddle-glow: rgba(0, 255, 255, 0.6); 183 | --ball-color: rgba(255, 255, 255, 0.95); 184 | --ball-glow: rgba(135, 206, 235, 0.8); 185 | --center-line-color: rgba(0, 191, 255, 0.4); 186 | 187 | /* UI Elements */ 188 | --text-color: #87ceeb; 189 | --title-gradient-1: #00bfff; 190 | --title-gradient-2: #1e90ff; 191 | --title-gradient-3: #00ffff; 192 | --title-gradient-4: #87ceeb; 193 | --title-glow: rgba(135, 206, 235, 0.6); 194 | 195 | /* Controls and buttons */ 196 | --control-bg: rgba(0, 191, 255, 0.1); 197 | --control-border: rgba(0, 191, 255, 0.3); 198 | --control-glow: rgba(0, 191, 255, 0.25); 199 | --button-bg: rgba(0, 191, 255, 0.15); 200 | --button-border: rgba(0, 191, 255, 0.4); 201 | --button-hover-bg: rgba(0, 191, 255, 0.25); 202 | --button-hover-border: rgba(0, 255, 255, 0.6); 203 | 204 | /* Screen overlays */ 205 | --overlay-bg: rgba(0, 17, 34, 0.6); 206 | --welcome-title-gradient-1: #00bfff; 207 | --welcome-title-gradient-2: #00ffff; 208 | --welcome-title-gradient-3: #87ceeb; 209 | --gameover-title-gradient-1: #00ffff; 210 | --gameover-title-gradient-2: #87ceeb; 211 | --gameover-title-gradient-3: #1e90ff; 212 | --gameover-glow: rgba(0, 255, 255, 0.7); 213 | 214 | /* Particle effects */ 215 | --particle-color: rgba(0, 191, 255, 0.15); 216 | } 217 | 218 | /* ===== THEME-SPECIFIC ANIMATIONS ===== */ 219 | 220 | /* Neon Retro - More intense pulsing */ 221 | .theme-neon-retro canvas { 222 | animation: neonPulse 2s ease-in-out infinite; 223 | } 224 | 225 | @keyframes neonPulse { 226 | 0%, 100% { 227 | box-shadow: 228 | 0 8px 32px 0 var(--canvas-glow-color), 229 | inset 0 0 50px rgba(255, 0, 153, 0.2); 230 | } 231 | 50% { 232 | box-shadow: 233 | 0 12px 40px 0 var(--canvas-glow-color), 234 | inset 0 0 60px rgba(255, 0, 153, 0.4); 235 | } 236 | } 237 | 238 | /* Dark Mode - Subtle glow */ 239 | .theme-dark-mode canvas { 240 | animation: subtleGlow 6s ease-in-out infinite; 241 | } 242 | 243 | @keyframes subtleGlow { 244 | 0%, 100% { 245 | box-shadow: 246 | 0 4px 16px 0 var(--canvas-glow-color), 247 | inset 0 0 20px rgba(128, 128, 128, 0.1); 248 | } 249 | 50% { 250 | box-shadow: 251 | 0 6px 20px 0 var(--canvas-glow-color), 252 | inset 0 0 25px rgba(128, 128, 128, 0.15); 253 | } 254 | } 255 | 256 | /* Ocean Blue - Wave-like animation */ 257 | .theme-ocean-blue canvas { 258 | animation: oceanWave 5s ease-in-out infinite; 259 | } 260 | 261 | @keyframes oceanWave { 262 | 0%, 100% { 263 | box-shadow: 264 | 0 8px 32px 0 var(--canvas-glow-color), 265 | inset 0 0 40px rgba(0, 191, 255, 0.15); 266 | } 267 | 50% { 268 | box-shadow: 269 | 0 12px 40px 0 var(--canvas-glow-color), 270 | inset 0 0 50px rgba(0, 191, 255, 0.25); 271 | } 272 | } 273 | 274 | /* ===== RESPONSIVE THEME ADJUSTMENTS ===== */ 275 | @media (max-width: 768px) { 276 | /* Reduce animation intensity on mobile for all themes */ 277 | .theme-neon-retro canvas, 278 | .theme-dark-mode canvas, 279 | .theme-ocean-blue canvas { 280 | animation-duration: 8s; 281 | } 282 | 283 | /* Adjust particle effects for mobile performance */ 284 | .theme-neon-retro body::before { 285 | opacity: 0.6; 286 | } 287 | 288 | .theme-dark-mode body::before { 289 | opacity: 0.3; 290 | } 291 | 292 | .theme-ocean-blue body::before { 293 | opacity: 0.5; 294 | } 295 | } 296 | 297 | @media (prefers-reduced-motion: reduce) { 298 | /* Disable theme-specific animations for accessibility */ 299 | .theme-neon-retro canvas, 300 | .theme-dark-mode canvas, 301 | .theme-ocean-blue canvas { 302 | animation: none !important; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // script.js 2 | window.addEventListener("DOMContentLoaded", () => { 3 | // ---- DOM refs ---- 4 | const canvas = document.getElementById("gameCanvas"); 5 | const ctx = canvas.getContext("2d"); 6 | 7 | const welcomeScreen = document.getElementById("welcomeScreen"); 8 | const startGameButton = document.getElementById("startGameButton"); 9 | 10 | const gameOverScreen = document.getElementById("gameOverScreen"); 11 | const gameOverMessage = document.getElementById("gameOverMessage"); 12 | const playAgainButton = document.getElementById("playAgainButton"); 13 | 14 | const pauseButton = document.getElementById("pauseButton"); 15 | const restartButton = document.getElementById("restartButton"); 16 | const difficultySelect = document.getElementById("difficulty"); 17 | const fullscreenButton = document.getElementById("fullscreenButton"); 18 | const themeSelector = document.getElementById("themeSelector"); 19 | 20 | const playerScoreDisplay = document.getElementById("playerScore"); 21 | const aiScoreDisplay = document.getElementById("aiScore"); 22 | 23 | // How to Play modal 24 | const howToPlayButton = document.getElementById("howToPlayButton"); 25 | const howToPlayModal = document.getElementById("howToPlayModal"); 26 | const closeHowToPlay = document.getElementById("closeHowToPlay"); 27 | const closeHowToPlayBtn = document.getElementById("closeHowToPlayBtn"); 28 | 29 | // ---- Game state ---- 30 | let gameRunning = false; 31 | let paused = false; 32 | let playerScore = 0; 33 | let aiScore = 0; 34 | let difficulty = "medium"; 35 | let playerName = ""; 36 | 37 | // ---- Objects ---- 38 | const paddleWidth = 10; 39 | const paddleHeight = 80; 40 | 41 | const player = { x: 10, y: canvas.height / 2 - paddleHeight / 2, dy: 6 }; 42 | const ai = { x: canvas.width - 20, y: canvas.height / 2 - paddleHeight / 2, dy: 5 }; 43 | 44 | const ball = { x: canvas.width / 2, y: canvas.height / 2, r: 8, dx: 4, dy: 4 }; 45 | 46 | const speeds = { 47 | easy: 3, 48 | medium: 5, 49 | hard: 7 50 | }; 51 | 52 | // ---- Theme functionality ---- 53 | themeSelector.addEventListener("change", (e) => { 54 | // Remove all theme classes 55 | document.body.classList.remove("neon-retro", "dark-mode", "ocean-blue"); 56 | 57 | // Add the selected theme class 58 | if (e.target.value !== "default") { 59 | document.body.classList.add(e.target.value); 60 | } 61 | }); 62 | 63 | // ---- Fullscreen functionality ---- 64 | fullscreenButton.addEventListener("click", toggleFullscreen); 65 | 66 | function toggleFullscreen() { 67 | if (!document.fullscreenElement) { 68 | document.documentElement.requestFullscreen().catch(err => { 69 | console.error(`Error attempting to enable fullscreen: ${err.message}`); 70 | }); 71 | fullscreenButton.textContent = "❐"; 72 | } else { 73 | if (document.exitFullscreen) { 74 | document.exitFullscreen(); 75 | fullscreenButton.textContent = "⛶"; 76 | } 77 | } 78 | } 79 | 80 | // ---- Helpers ---- 81 | function updateScoreUI() { 82 | const displayName = playerName || "Player"; 83 | playerScoreDisplay.textContent = `${displayName}: ${playerScore}`; 84 | aiScoreDisplay.textContent = `AI: ${aiScore}`; 85 | // Update new scoreboard 86 | document.getElementById('scorePlayerName').textContent = displayName; 87 | document.getElementById('scorePlayerAvatar').textContent = window.selectedAvatar || "😎"; 88 | document.getElementById('playerScore').textContent = playerScore; 89 | document.getElementById('aiScore').textContent = aiScore; 90 | } 91 | 92 | function resetBall() { 93 | ball.x = canvas.width / 2; 94 | ball.y = canvas.height / 2; 95 | ball.dx *= -1; // send it toward the scorer 96 | ball.dy = (Math.random() * 2 - 1) * 4; // small random vertical 97 | } 98 | 99 | function centerPaddles() { 100 | player.y = canvas.height / 2 - paddleHeight / 2; 101 | ai.y = canvas.height / 2 - paddleHeight / 2; 102 | } 103 | 104 | // ---- Draw ---- 105 | function draw() { 106 | ctx.clearRect(0, 0, canvas.width, canvas.height); 107 | 108 | // Background 109 | ctx.fillStyle = "black"; 110 | ctx.fillRect(0, 0, canvas.width, canvas.height); 111 | 112 | // Net 113 | ctx.fillStyle = "#4cc9f0"; 114 | for (let i = 0; i < canvas.height; i += 20) { 115 | ctx.fillRect(canvas.width / 2 - 1, i, 2, 10); 116 | } 117 | 118 | // Paddles 119 | ctx.fillStyle = "#4cc9f0"; 120 | ctx.fillRect(player.x, player.y, paddleWidth, paddleHeight); 121 | 122 | ctx.fillStyle = "#f72585"; 123 | ctx.fillRect(ai.x, ai.y, paddleWidth, paddleHeight); 124 | 125 | // Ball 126 | ctx.fillStyle = "white"; 127 | ctx.beginPath(); 128 | ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2); 129 | ctx.fill(); 130 | 131 | // Canvas score (optional—DOM already shows the score) 132 | ctx.font = "16px Arial"; 133 | ctx.fillStyle = "#4cc9f0"; 134 | const displayName = playerName || "Player"; 135 | ctx.fillText(`${displayName}: ${playerScore}`, 50, 30); 136 | 137 | ctx.fillStyle = "#f72585"; 138 | ctx.fillText(`AI: ${aiScore}`, canvas.width - 150, 30); 139 | } 140 | 141 | // ---- Update ---- 142 | function update() { 143 | // Move ball 144 | ball.x += ball.dx; 145 | ball.y += ball.dy; 146 | 147 | // Bounce top/bottom 148 | if (ball.y - ball.r < 0 || ball.y + ball.r > canvas.height) { 149 | ball.dy *= -1; 150 | } 151 | 152 | // Player collision 153 | if ( 154 | ball.x - ball.r <= player.x + paddleWidth && 155 | ball.y >= player.y && 156 | ball.y <= player.y + paddleHeight && 157 | ball.dx < 0 158 | ) { 159 | ball.dx *= -1; 160 | // add a little "spin" based on hit position 161 | const collidePoint = ball.y - (player.y + paddleHeight / 2); 162 | ball.dy = collidePoint * 0.15; 163 | } 164 | 165 | // AI collision 166 | if ( 167 | ball.x + ball.r >= ai.x && 168 | ball.y >= ai.y && 169 | ball.y <= ai.y + paddleHeight && 170 | ball.dx > 0 171 | ) { 172 | ball.dx *= -1; 173 | const collidePoint = ball.y - (ai.y + paddleHeight / 2); 174 | ball.dy = collidePoint * 0.15; 175 | } 176 | 177 | // Score (ball passes left/right) 178 | if (ball.x + ball.r < 0) { 179 | aiScore++; 180 | updateScoreUI(); 181 | resetBall(); 182 | } else if (ball.x - ball.r > canvas.width) { 183 | playerScore++; 184 | updateScoreUI(); 185 | resetBall(); 186 | } 187 | 188 | // Simple AI: follow ball at difficulty speed 189 | const aiCenter = ai.y + paddleHeight / 2; 190 | if (ball.y < aiCenter) ai.y -= speeds[difficulty]; 191 | else if (ball.y > aiCenter) ai.y += speeds[difficulty]; 192 | 193 | // Bound paddles 194 | player.y = Math.max(0, Math.min(canvas.height - paddleHeight, player.y)); 195 | ai.y = Math.max(0, Math.min(canvas.height - paddleHeight, ai.y)); 196 | 197 | // Game over: first to 3, lead by 2 198 | if ((playerScore >= 3 || aiScore >= 3) && Math.abs(playerScore - aiScore) >= 2) { 199 | endGame(); 200 | } 201 | } 202 | 203 | // ---- Loop ---- 204 | function loop() { 205 | if (gameRunning && !paused) { 206 | update(); 207 | draw(); 208 | } 209 | requestAnimationFrame(loop); 210 | } 211 | requestAnimationFrame(loop); 212 | 213 | // ---- Controls ---- 214 | // Keyboard (hold to move) 215 | const keys = { up: false, down: false }; 216 | document.addEventListener("keydown", (e) => { 217 | if (e.key === "ArrowUp") keys.up = true; 218 | if (e.key === "ArrowDown") keys.down = true; 219 | 220 | // Pause with spacebar 221 | if (e.key === " " && gameRunning) { 222 | paused = !paused; 223 | pauseButton.textContent = paused ? "Resume" : "Pause"; 224 | } 225 | }); 226 | document.addEventListener("keyup", (e) => { 227 | if (e.key === "ArrowUp") keys.up = false; 228 | if (e.key === "ArrowDown") keys.down = false; 229 | }); 230 | 231 | // Apply keyboard movement each frame by piggybacking on the RAF loop 232 | (function applyKeyboardMotion() { 233 | if (gameRunning && !paused) { 234 | if (keys.up) player.y -= player.dy; 235 | if (keys.down) player.y += player.dy; 236 | } 237 | requestAnimationFrame(applyKeyboardMotion); 238 | })(); 239 | 240 | // Mouse / Touch move (center paddle on pointer) 241 | canvas.addEventListener("mousemove", (evt) => { 242 | const rect = canvas.getBoundingClientRect(); 243 | const mouseY = evt.clientY - rect.top; 244 | player.y = mouseY - paddleHeight / 2; 245 | }); 246 | 247 | canvas.addEventListener( 248 | "touchstart", 249 | (evt) => { 250 | evt.preventDefault(); 251 | const rect = canvas.getBoundingClientRect(); 252 | const touchY = evt.touches[0].clientY - rect.top; 253 | player.y = touchY - paddleHeight / 2; 254 | }, 255 | { passive: false } 256 | ); 257 | 258 | canvas.addEventListener( 259 | "touchmove", 260 | (evt) => { 261 | evt.preventDefault(); 262 | const rect = canvas.getBoundingClientRect(); 263 | const touchY = evt.touches[0].clientY - rect.top; 264 | player.y = touchY - paddleHeight / 2; 265 | }, 266 | { passive: false } 267 | ); 268 | 269 | // ---- Buttons / UI ---- 270 | function startGame() { 271 | playerName = document.getElementById("playerNameInput").value.trim(); 272 | gameRunning = true; 273 | paused = false; 274 | pauseButton.textContent = "Pause"; 275 | playerScore = 0; 276 | aiScore = 0; 277 | updateScoreUI(); 278 | centerPaddles(); 279 | resetBall(); 280 | if (welcomeScreen) welcomeScreen.style.display = "none"; 281 | if (gameOverScreen) gameOverScreen.style.display = "none"; 282 | // Optionally hide profile preview after starting 283 | document.getElementById('profilePreview').classList.add('hidden'); 284 | } 285 | 286 | function endGame() { 287 | gameRunning = false; 288 | const displayName = playerName || "Player"; 289 | if (playerScore > aiScore) { 290 | gameOverMessage.textContent = `🎉 ${displayName} Wins!`; 291 | gameOverScreen.classList.add("winner"); 292 | gameOverScreen.classList.remove("loser"); 293 | } else { 294 | gameOverMessage.textContent = "😢 AI Wins!"; 295 | gameOverScreen.classList.add("loser"); 296 | gameOverScreen.classList.remove("winner"); 297 | } 298 | gameOverScreen.style.display = "flex"; 299 | } 300 | 301 | startGameButton.addEventListener("click", startGame); 302 | playAgainButton.addEventListener("click", startGame); 303 | 304 | pauseButton.addEventListener("click", () => { 305 | if (!gameRunning) return; 306 | paused = !paused; 307 | pauseButton.textContent = paused ? "Resume" : "Pause"; 308 | }); 309 | 310 | restartButton.addEventListener("click", () => { 311 | if (!gameRunning) gameRunning = true; 312 | paused = false; 313 | pauseButton.textContent = "Pause"; 314 | playerScore = 0; 315 | aiScore = 0; 316 | updateScoreUI(); 317 | centerPaddles(); 318 | resetBall(); 319 | gameOverScreen.style.display = "none"; 320 | }); 321 | 322 | difficultySelect.addEventListener("change", (e) => { 323 | difficulty = e.target.value; 324 | }); 325 | 326 | // ---- Enhanced How to Play Modal ---- 327 | function openHowTo() { 328 | if (howToPlayModal) { 329 | howToPlayModal.classList.remove("hidden"); 330 | howToPlayModal.classList.add("active"); 331 | document.body.style.overflow = "hidden"; // Prevent scrolling when modal is open 332 | } 333 | } 334 | 335 | function closeHowTo() { 336 | if (howToPlayModal) { 337 | howToPlayModal.classList.remove("active"); 338 | setTimeout(() => { 339 | howToPlayModal.classList.add("hidden"); 340 | }, 300); // Match the transition duration 341 | document.body.style.overflow = ""; // Re-enable scrolling 342 | } 343 | } 344 | 345 | if (howToPlayButton && howToPlayModal && closeHowToPlay && closeHowToPlayBtn) { 346 | // open on button 347 | howToPlayButton.addEventListener("click", openHowTo); 348 | 349 | // close on "Close" button 350 | closeHowToPlay.addEventListener("click", closeHowTo); 351 | closeHowToPlayBtn.addEventListener("click", closeHowTo); 352 | 353 | // close on overlay click 354 | howToPlayModal.addEventListener("click", (e) => { 355 | if (e.target === howToPlayModal) closeHowTo(); 356 | }); 357 | 358 | // close on Escape 359 | document.addEventListener("keydown", (e) => { 360 | if (e.key === "Escape" && howToPlayModal.classList.contains("active")) { 361 | closeHowTo(); 362 | } 363 | }); 364 | } 365 | 366 | // --- Avatar selection and profile preview --- 367 | window.selectedAvatar = document.querySelector('.avatar-option.selected')?.getAttribute('data-avatar') || "😎"; 368 | 369 | document.querySelectorAll('.avatar-option').forEach(option => { 370 | option.addEventListener('click', function() { 371 | document.querySelectorAll('.avatar-option').forEach(o => o.classList.remove('selected')); 372 | this.classList.add('selected'); 373 | window.selectedAvatar = this.getAttribute('data-avatar'); 374 | updateProfilePreview(); 375 | updateScoreUI(); 376 | }); 377 | }); 378 | 379 | const playerNameInput = document.getElementById('playerNameInput'); 380 | if (playerNameInput) { 381 | playerNameInput.addEventListener('input', function() { 382 | updateProfilePreview(); 383 | updateScoreUI(); 384 | }); 385 | } 386 | 387 | function updateProfilePreview() { 388 | const name = playerNameInput.value.trim(); 389 | const avatar = window.selectedAvatar || document.querySelector('.avatar-option.selected').getAttribute('data-avatar'); 390 | const preview = document.getElementById('profilePreview'); 391 | document.getElementById('profileAvatar').textContent = avatar; 392 | document.getElementById('profileName').textContent = name ? name : ''; 393 | preview.classList.toggle('hidden', !name); 394 | } 395 | 396 | // Show profile preview when name is entered 397 | updateProfilePreview(); 398 | 399 | // When starting the game, use the selected avatar and name 400 | function startGame() { 401 | playerName = playerNameInput.value.trim(); 402 | // You can use window.selectedAvatar for the player's avatar in your game logic 403 | gameRunning = true; 404 | paused = false; 405 | pauseButton.textContent = "Pause"; 406 | playerScore = 0; 407 | aiScore = 0; 408 | updateScoreUI(); 409 | centerPaddles(); 410 | resetBall(); 411 | if (welcomeScreen) welcomeScreen.style.display = "none"; 412 | if (gameOverScreen) gameOverScreen.style.display = "none"; 413 | // Optionally hide profile preview after starting 414 | document.getElementById('profilePreview').classList.add('hidden'); 415 | } 416 | }); -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* ===== GENERAL STYLING ===== */ 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Segoe UI', Arial, sans-serif; 7 | background: #181825; 8 | color: #fff; 9 | } 10 | 11 | /* ===== WELCOME SCREEN ONLY ===== */ 12 | .welcome-screen { 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | width: 100vw; 17 | height: 100vh; 18 | background: radial-gradient(ellipse at center, #23243a 0%, #181825 70%, #120532 100%); 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | align-items: center; 23 | z-index: 100; 24 | gap: 32px; 25 | box-shadow: 0 0 60px 10px #4cc9f0a0; 26 | overflow: hidden; 27 | animation: welcomeFadeIn 0.8s ease-out; 28 | } 29 | 30 | @keyframes welcomeFadeIn { 31 | from { opacity: 0; } 32 | to { opacity: 1; } 33 | } 34 | 35 | .welcome-screen::before { 36 | content: ''; 37 | position: absolute; 38 | inset: 0; 39 | z-index: 0; 40 | opacity: 0.18; 41 | background-image: 42 | repeating-linear-gradient(0deg, #4cc9f0 0px, #4cc9f0 6px, transparent 6px, transparent 64px), 43 | repeating-linear-gradient(90deg, #f72585 0px, #f72585 6px, transparent 6px, transparent 64px); 44 | background-size: 64px 64px, 64px 64px; 45 | background-repeat: repeat; 46 | animation: gridColorMove 8s linear infinite; 47 | pointer-events: none; 48 | box-shadow: 0 0 32px 8px #4cc9f0, 0 0 48px 16px #f72585; 49 | background-blend-mode: lighten; 50 | } 51 | 52 | @keyframes gridColorMove { 53 | 0% { 54 | background-position: 0 0, 0 0; 55 | filter: hue-rotate(0deg); 56 | } 57 | 50% { 58 | background-position: 32px 32px, 32px 32px; 59 | filter: hue-rotate(60deg); 60 | } 61 | 100% { 62 | background-position: 64px 64px, 64px 64px; 63 | filter: hue-rotate(0deg); 64 | } 65 | } 66 | 67 | .welcome-screen::after { 68 | content: ''; 69 | position: absolute; 70 | inset: 0; 71 | z-index: 0; 72 | pointer-events: none; 73 | background: radial-gradient(ellipse at center, transparent 60%, #181825 100%); 74 | opacity: 0.6; 75 | animation: none; 76 | } 77 | 78 | .welcome-screen h2 { 79 | font-size: 2.7rem; 80 | margin-bottom: 20px; 81 | color: #fff; 82 | text-shadow: 0 0 18px #4cc9f0, 0 0 32px #f72585; 83 | animation: titleBounce 1.5s infinite alternate; 84 | letter-spacing: 2px; 85 | background: none; 86 | -webkit-background-clip: initial; 87 | -webkit-text-fill-color: initial; 88 | position: relative; 89 | z-index: 1; 90 | } 91 | 92 | @keyframes titleBounce { 93 | 0% { transform: translateY(0); } 94 | 100% { transform: translateY(-8px); } 95 | } 96 | 97 | .welcome-screen h2 .pong-emoji { 98 | text-shadow: none !important; 99 | background: none; 100 | -webkit-background-clip: initial; 101 | -webkit-text-fill-color: initial; 102 | color: inherit; 103 | filter: none; 104 | font-size: 3.2rem; 105 | margin: 0 10px; 106 | vertical-align: middle; 107 | animation: emojiWobble 0.8s infinite alternate; 108 | transition: transform 0.2s; 109 | z-index: 1; 110 | } 111 | 112 | @keyframes emojiWobble { 113 | 0% { transform: rotate(-10deg) scale(1); } 114 | 50% { transform: rotate(10deg) scale(1.15); } 115 | 100% { transform: rotate(-10deg) scale(1); } 116 | } 117 | 118 | .avatar-select-container { 119 | display: flex; 120 | flex-direction: column; 121 | align-items: center; 122 | gap: 10px; 123 | margin-bottom: 10px; 124 | z-index: 2; 125 | } 126 | 127 | .avatar-select-label { 128 | font-size: 1.1rem; 129 | color: #4cc9f0; 130 | margin-bottom: 4px; 131 | letter-spacing: 1px; 132 | text-shadow: 0 0 8px #23243a; 133 | } 134 | 135 | .avatar-options { 136 | display: flex; 137 | gap: 16px; 138 | justify-content: center; 139 | } 140 | 141 | .avatar-option { 142 | width: 54px; 143 | height: 54px; 144 | border-radius: 50%; 145 | background: #23243a; 146 | border: 2.5px solid #4cc9f0; 147 | display: flex; 148 | align-items: center; 149 | justify-content: center; 150 | font-size: 2.1rem; 151 | cursor: pointer; 152 | transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; 153 | box-shadow: 0 0 12px #4cc9f040; 154 | position: relative; 155 | user-select: none; 156 | } 157 | 158 | .avatar-option.selected, 159 | .avatar-option:hover { 160 | border-color: #f72585; 161 | box-shadow: 0 0 18px #f7258580; 162 | transform: scale(1.18) rotate(-8deg); 163 | } 164 | 165 | .avatar-option::after { 166 | content: ''; 167 | display: block; 168 | position: absolute; 169 | inset: 0; 170 | border-radius: 50%; 171 | pointer-events: none; 172 | box-shadow: 0 0 0 0 #f72585; 173 | transition: box-shadow 0.2s; 174 | } 175 | 176 | .avatar-option.selected::after { 177 | box-shadow: 0 0 12px 4px #f7258580; 178 | } 179 | 180 | .profile-preview { 181 | display: flex; 182 | align-items: center; 183 | gap: 14px; 184 | margin: 18px 0 0 0; 185 | font-size: 1.3rem; 186 | background: rgba(44, 45, 70, 0.7); 187 | border-radius: 12px; 188 | padding: 10px 22px; 189 | box-shadow: 0 0 12px #4cc9f0; 190 | color: #fff; 191 | z-index: 2; 192 | transition: all 0.3s; 193 | } 194 | 195 | .profile-avatar { 196 | font-size: 2.2rem; 197 | border-radius: 50%; 198 | background: #23243a; 199 | border: 2px solid #f72585; 200 | padding: 6px; 201 | box-shadow: 0 0 8px #f72585; 202 | } 203 | 204 | .profile-name { 205 | font-weight: bold; 206 | letter-spacing: 1px; 207 | font-size: 1.2rem; 208 | } 209 | 210 | #playerNameInput { 211 | padding: 14px 24px; 212 | width: 320px; 213 | border: 2px solid #4cc9f0; 214 | border-radius: 12px; 215 | background: rgba(44, 45, 70, 0.85); 216 | color: #fff; 217 | font-size: 1.1rem; 218 | text-align: center; 219 | box-shadow: 0 0 8px #4cc9f0; 220 | transition: all 0.3s ease; 221 | z-index: 1; 222 | } 223 | 224 | #playerNameInput:focus { 225 | outline: none; 226 | border-color: #f72585; 227 | box-shadow: 0 0 14px #f72585; 228 | background: rgba(44, 45, 70, 0.95); 229 | } 230 | 231 | .welcome-screen button { 232 | padding: 14px 36px; 233 | font-size: 1.2rem; 234 | background: linear-gradient(90deg, #4cc9f0 40%, #f72585 100%); 235 | color: #fff; 236 | border-radius: 30px; 237 | box-shadow: 0 0 12px #4cc9f0, 0 0 6px #f72585 inset; 238 | border: none; 239 | animation: buttonPulse 2s infinite; 240 | transition: background 0.3s, box-shadow 0.3s, transform 0.2s; 241 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 242 | font-weight: bold; 243 | letter-spacing: 1px; 244 | position: relative; 245 | overflow: hidden; 246 | z-index: 1; 247 | } 248 | 249 | @keyframes buttonPulse { 250 | 0%, 100% { box-shadow: 0 0 12px #4cc9f0, 0 0 6px #f72585 inset; } 251 | 50% { box-shadow: 0 0 24px #f72585, 0 0 12px #4cc9f0 inset; } 252 | } 253 | 254 | .welcome-screen button:hover { 255 | background: linear-gradient(90deg, #4895ef 40%, #b5179e 100%); 256 | box-shadow: 0 0 18px #f72585, 0 0 8px #4cc9f0 inset; 257 | transform: scale(1.07) rotate(-2deg); 258 | } 259 | 260 | /* Hide elements utility */ 261 | .hidden { 262 | display: none !important; 263 | } 264 | 265 | /* ===== MAIN GAME PAGE STYLES ===== */ 266 | .main-game-container { 267 | display: flex; 268 | flex-direction: column; 269 | align-items: center; 270 | justify-content: flex-start; 271 | min-height: 100vh; 272 | padding-top: 0; /* Remove extra top padding */ 273 | margin-top: 18px; /* Small margin for breathing room */ 274 | z-index: 1; 275 | } 276 | 277 | .main-game-container h1 { 278 | font-size: 2.2rem; 279 | margin-bottom: 18px; 280 | color: #fff; 281 | text-shadow: 0 0 8px #4cc9f0; 282 | } 283 | 284 | .score { 285 | font-size: 1.2rem; 286 | color: #fff; 287 | margin: 10px 0; 288 | text-align: center; 289 | letter-spacing: 1px; 290 | text-shadow: 0 0 8px #4cc9f0; 291 | } 292 | 293 | .game-controls { 294 | display: flex; 295 | gap: 18px; 296 | justify-content: center; 297 | margin: 18px 0; 298 | } 299 | 300 | canvas#gameCanvas { 301 | background: linear-gradient(135deg, #181825 60%, #23243a 100%); 302 | border: 3px solid #4cc9f0; 303 | border-radius: 18px; 304 | box-shadow: 0 0 32px #4cc9f0, 0 0 24px #f72585 inset; 305 | margin-bottom: 18px; 306 | transition: box-shadow 0.3s; 307 | } 308 | 309 | /* Modal Styles (How to Play) */ 310 | .modal-content { 311 | background: linear-gradient(135deg, #23243a 60%, #4cc9f0 100%); 312 | padding: 32px; 313 | border-radius: 18px; 314 | max-width: 500px; 315 | width: 92%; 316 | box-shadow: 0 0 40px #4cc9f0, 0 0 80px #f72585; 317 | border: 2.5px solid #f72585; 318 | position: relative; 319 | transform: translateY(-50px) scale(0.92); 320 | transition: transform 0.5s ease; 321 | color: #fff; 322 | overflow: hidden; 323 | } 324 | 325 | .modal.active .modal-content { 326 | transform: translateY(0) scale(1); 327 | } 328 | 329 | .modal-content h3 { 330 | font-size: 2rem; 331 | margin-bottom: 18px; 332 | color: #fff; 333 | text-align: center; 334 | text-shadow: 0 0 18px #4cc9f0, 0 0 32px #f72585; 335 | letter-spacing: 2px; 336 | } 337 | 338 | .modal-content ul { 339 | margin: 20px 0; 340 | padding-left: 0; 341 | list-style: none; 342 | } 343 | 344 | .modal-content li { 345 | margin-bottom: 16px; 346 | line-height: 1.7; 347 | padding: 12px 18px; 348 | border-radius: 10px; 349 | background: rgba(44, 45, 70, 0.85); 350 | border-left: 6px solid #f72585; 351 | font-size: 1.1rem; 352 | box-shadow: 0 0 8px #4cc9f0; 353 | transition: all 0.3s ease; 354 | position: relative; 355 | } 356 | 357 | .modal-content li::before { 358 | content: "🎮"; 359 | margin-right: 10px; 360 | font-size: 1.2rem; 361 | vertical-align: middle; 362 | } 363 | 364 | .modal-content li:hover { 365 | transform: translateX(8px) scale(1.04); 366 | border-left: 6px solid #4cc9f0; 367 | background: rgba(76, 201, 240, 0.15); 368 | } 369 | 370 | #closeHowToPlayBtn, #closeHowToPlay { 371 | display: block; 372 | margin: 24px auto 0; 373 | padding: 12px 32px; 374 | background: linear-gradient(90deg, #4cc9f0, #f72585); 375 | border-radius: 30px; 376 | color: #fff; 377 | font-weight: bold; 378 | font-size: 1.1rem; 379 | border: none; 380 | box-shadow: 0 0 12px #4cc9f0; 381 | cursor: pointer; 382 | transition: background 0.3s, box-shadow 0.3s; 383 | } 384 | 385 | #closeHowToPlayBtn:hover, #closeHowToPlay:hover { 386 | background: linear-gradient(90deg, #4895ef, #b5179e); 387 | box-shadow: 0 0 18px #f72585; 388 | transform: scale(1.07); 389 | } 390 | 391 | .scoreboard { 392 | display: flex; 393 | justify-content: center; 394 | gap: 60px; 395 | margin: 0 0 16px 0; /* No top margin, less bottom margin */ 396 | font-size: 1.3rem; 397 | background: rgba(44, 45, 70, 0.92); 398 | border-radius: 16px; 399 | box-shadow: 0 0 18px #4cc9f0; 400 | padding: 18px 40px; 401 | position: relative; 402 | top: 0; 403 | } 404 | 405 | .score-player, .score-ai { 406 | display: flex; 407 | align-items: center; 408 | gap: 14px; 409 | } 410 | 411 | .score-avatar { 412 | font-size: 2.1rem; 413 | border-radius: 50%; 414 | background: #23243a; 415 | border: 2px solid #f72585; 416 | padding: 4px 8px; 417 | box-shadow: 0 0 8px #f72585; 418 | } 419 | 420 | .score-name { 421 | font-weight: bold; 422 | font-size: 1.1rem; 423 | color: #4cc9f0; 424 | letter-spacing: 1px; 425 | text-shadow: 0 0 8px #23243a; 426 | } 427 | 428 | .score-value { 429 | font-size: 1.4rem; 430 | font-weight: bold; 431 | color: #fff; 432 | margin-left: 8px; 433 | text-shadow: 0 0 8px #f72585; 434 | } 435 | 436 | /* Interactive select dropdowns */ 437 | .game-controls select, 438 | #difficulty, 439 | #themeSelector { 440 | padding: 10px 18px; 441 | font-size: 1.05rem; 442 | border-radius: 18px; 443 | border: 2px solid #4cc9f0; 444 | background: linear-gradient(90deg, #23243a 60%, #4cc9f0 100%); 445 | color: #fff; 446 | font-weight: bold; 447 | box-shadow: 0 0 8px #4cc9f0; 448 | margin: 0 8px; 449 | transition: border-color 0.3s, box-shadow 0.3s; 450 | } 451 | 452 | .game-controls select:focus, 453 | #difficulty:focus, 454 | #themeSelector:focus { 455 | border-color: #f72585; 456 | box-shadow: 0 0 14px #f72585; 457 | outline: none; 458 | } 459 | 460 | /* Nicer buttons */ 461 | .game-controls button, 462 | #pauseButton, #restartButton, #fullscreenButton { 463 | padding: 12px 28px; 464 | font-size: 1.1rem; 465 | background: linear-gradient(90deg, #4cc9f0 40%, #f72585 100%); 466 | color: #fff; 467 | border-radius: 24px; 468 | border: none; 469 | box-shadow: 0 0 12px #4cc9f0, 0 0 6px #f72585 inset; 470 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 471 | font-weight: bold; 472 | letter-spacing: 1px; 473 | cursor: pointer; 474 | margin: 0 6px; 475 | transition: 476 | background 0.3s, 477 | box-shadow 0.3s, 478 | transform 0.2s, 479 | filter 0.2s; 480 | position: relative; 481 | overflow: hidden; 482 | } 483 | 484 | .game-controls button:active, 485 | #pauseButton:active, #restartButton:active, #fullscreenButton:active { 486 | filter: brightness(1.2) drop-shadow(0 0 8px #f72585); 487 | transform: scale(0.97); 488 | } 489 | 490 | .game-controls button:hover, 491 | #pauseButton:hover, #restartButton:hover, #fullscreenButton:hover { 492 | background: linear-gradient(90deg, #4895ef 40%, #b5179e 100%); 493 | box-shadow: 0 0 18px #f72585, 0 0 8px #4cc9f0 inset; 494 | transform: scale(1.07) rotate(-2deg); 495 | } 496 | 497 | /* Game Over Screen - show winner/loser differently */ 498 | .game-over-screen { 499 | display: none; 500 | flex-direction: column; 501 | align-items: center; 502 | justify-content: center; 503 | position: fixed; 504 | top: 0; left: 0; 505 | width: 100vw; height: 100vh; 506 | background: rgba(44,45,70,0.95); 507 | z-index: 200; 508 | animation: fadeIn 0.6s; 509 | } 510 | 511 | @keyframes fadeIn { 512 | from { opacity: 0; } 513 | to { opacity: 1; } 514 | } 515 | 516 | #gameOverMessage { 517 | font-size: 2.2rem; 518 | margin-bottom: 28px; 519 | color: #fff; 520 | text-align: center; 521 | text-shadow: 0 0 18px #4cc9f0, 0 0 32px #f72585; 522 | letter-spacing: 2px; 523 | padding: 18px 36px; 524 | border-radius: 16px; 525 | background: linear-gradient(90deg, #23243a 60%, #f72585 100%); 526 | box-shadow: 0 0 24px #f72585; 527 | border: 2px solid #4cc9f0; 528 | } 529 | 530 | .game-over-screen.winner #gameOverMessage { 531 | background: linear-gradient(90deg, #23243a 60%, #4cc9f0 100%); 532 | color: #4cc9f0; 533 | border-color: #f72585; 534 | box-shadow: 0 0 32px #4cc9f0; 535 | } 536 | 537 | .game-over-screen.loser #gameOverMessage { 538 | background: linear-gradient(90deg, #23243a 60%, #f72585 100%); 539 | color: #f72585; 540 | border-color: #4cc9f0; 541 | box-shadow: 0 0 32px #f72585; 542 | } 543 | 544 | /* Make Play Again button more vibrant and animated */ 545 | #playAgainButton { 546 | padding: 16px 40px; 547 | font-size: 1.25rem; 548 | background: linear-gradient(90deg, #f72585 40%, #4cc9f0 100%); 549 | color: #fff; 550 | border-radius: 32px; 551 | border: none; 552 | box-shadow: 0 0 18px #f72585, 0 0 10px #4cc9f0 inset; 553 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 554 | font-weight: bold; 555 | letter-spacing: 1px; 556 | cursor: pointer; 557 | margin: 0 6px; 558 | transition: background 0.3s, box-shadow 0.3s, transform 0.2s; 559 | position: relative; 560 | overflow: hidden; 561 | animation: buttonPulse 1.5s infinite; 562 | } 563 | 564 | #playAgainButton:hover { 565 | background: linear-gradient(90deg, #4895ef 40%, #b5179e 100%); 566 | box-shadow: 0 0 24px #4cc9f0, 0 0 14px #f72585 inset; 567 | transform: scale(1.09) rotate(-2deg); 568 | } 569 | 570 | /* Make Difficulty and Theme labels stylish */ 571 | .game-controls label[for="difficulty"], 572 | .game-controls label[for="themeSelector"], 573 | label[for="difficulty"], 574 | label[for="themeSelector"] { 575 | font-size: 1.15rem; 576 | color: #4cc9f0; 577 | font-weight: bold; 578 | letter-spacing: 1px; 579 | margin-right: 10px; 580 | text-shadow: 0 0 10px #23243a, 0 0 8px #f72585; 581 | background: linear-gradient(90deg, #4cc9f0 60%, #f72585 100%); 582 | -webkit-background-clip: text; 583 | -webkit-text-fill-color: transparent; 584 | background-clip: text; 585 | text-fill-color: transparent; 586 | transition: text-shadow 0.3s; 587 | border-radius: 8px; 588 | padding: 2px 8px; 589 | display: inline-block; 590 | } 591 | 592 | .game-controls label[for="difficulty"]:hover, 593 | .game-controls label[for="themeSelector"]:hover, 594 | label[for="difficulty"]:hover, 595 | label[for="themeSelector"]:hover { 596 | text-shadow: 0 0 18px #f72585, 0 0 12px #4cc9f0; 597 | background: linear-gradient(90deg, #f72585 60%, #4cc9f0 100%); 598 | -webkit-background-clip: text; 599 | -webkit-text-fill-color: transparent; 600 | background-clip: text; 601 | text-fill-color: transparent; 602 | } 603 | 604 | /* Ping pong ball and paddle SVG animation for welcome screen */ 605 | .welcome-screen .pong-animation { 606 | position: absolute; 607 | bottom: 40px; 608 | right: 60px; 609 | width: 120px; 610 | height: 80px; 611 | z-index: 2; 612 | pointer-events: none; 613 | } 614 | 615 | .welcome-screen .pong-ball { 616 | position: absolute; 617 | left: 70px; 618 | top: 18px; 619 | width: 28px; 620 | height: 28px; 621 | animation: pongBallBounce 1.2s cubic-bezier(.5,1.5,.5,1) infinite; 622 | } 623 | 624 | .welcome-screen .pong-paddle { 625 | position: absolute; 626 | left: 10px; 627 | top: 38px; 628 | width: 80px; 629 | height: 28px; 630 | animation: pongPaddleMove 1.2s cubic-bezier(.5,1.5,.5,1) infinite; 631 | } 632 | 633 | @keyframes pongBallBounce { 634 | 0% { transform: translateY(0) scale(1); } 635 | 40% { transform: translateY(-32px) scale(1.1); } 636 | 60% { transform: translateY(-32px) scale(1.1); } 637 | 100% { transform: translateY(0) scale(1); } 638 | } 639 | 640 | @keyframes pongPaddleMove { 641 | 0% { transform: rotate(-8deg) scale(1); } 642 | 40% { transform: rotate(8deg) scale(1.05); } 643 | 60% { transform: rotate(8deg) scale(1.05); } 644 | 100% { transform: rotate(-8deg) scale(1); } 645 | } 646 | 647 | /* Add any other needed styles below... */ --------------------------------------------------------------------------------