Initial commit

This commit is contained in:
Allen
2026-04-30 02:14:25 +00:00
commit 7090e6f01d
243 changed files with 115122 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+16
View File
@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+29
View File
@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])
+26
View File
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg">
<link rel="apple-touch-icon" href="/icon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="GamerComp - Where kids become Game Composers! Learn AI communication by creating your own games.">
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#6366f1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="GamerComp">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="GamerComp">
<meta name="msapplication-TileColor" content="#6366f1">
<meta name="msapplication-tap-highlight" content="no">
<title>GamerComp - Compose Games with AI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+7548
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@fingerprintjs/fingerprintjs": "^5.0.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"tone": "^15.1.22"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"vite": "^7.2.4",
"vite-plugin-pwa": "^1.2.0",
"workbox-window": "^7.4.0"
}
}
File diff suppressed because it is too large Load Diff
+283
View File
@@ -0,0 +1,283 @@
# GamerComp Asset Library
A comprehensive procedural asset library for generating HTML5 canvas games. All assets are rendered in real-time using Canvas 2D API and Web Audio API.
## Quick Stats
| Asset Type | Count | Description |
|------------|-------|-------------|
| Backgrounds | 120 | 15 themes × 8 variants |
| Music | 160 | 8 moods × 20 songs |
| Avatar Modes | 4 | HEAD_ONLY, HEAD_AND_SHOULDERS, UPPER_BODY, FULL_BODY |
| Game Contexts | 26 | 12 vehicles, 10 costumes, 4 pure |
| Presets | 220 | 11 game styles × 20 presets |
## Directory Structure
```
/assets/
├── backgrounds/ # Procedural background system
│ ├── backgroundEngine.js # Core rendering engine
│ ├── effects.js # Particle/animation effects
│ ├── backgroundCatalog.js # Metadata catalog
│ └── themes/ # 15 theme files
│ ├── space.js
│ ├── nature.js
│ ├── urban.js
│ └── ... (12 more)
├── music/ # Procedural music system
│ ├── moodCategories.js # Mood definitions
│ ├── musicEngine.js # Tone.js synthesis engine
│ └── library/
│ ├── songs1-80.js # Songs 1-80
│ └── songs81-160.js # Songs 81-160
├── avatar/ # Mii-style avatar system
│ ├── avatarData.js # Face shapes, hair, colors
│ ├── accessories.js # 45 unlockable accessories
│ ├── animations.js # 31 animations
│ ├── avatarRenderer.js # Core avatar rendering
│ ├── accessoryRenderer.js # Accessory rendering
│ ├── avatarSystem.js # Main avatar API
│ ├── contextRenderer.js # Context rendering
│ ├── contextCatalog.js # Context metadata
│ └── contexts/
│ ├── vehicles.js # 12 vehicle contexts
│ ├── costumes.js # 10 costume contexts
│ └── pure.js # 4 pure contexts
├── assetManager.js # Unified asset coordination
├── assetCatalog.js # Combined metadata catalog
├── presets.js # 220 preset combinations
├── compatibility.js # Asset compatibility rules
└── README.md # This file
```
## Usage
### Basic Setup
Include the required scripts in your HTML:
```html
<!-- Backgrounds -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/space.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<!-- Music -->
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/music/musicEngine.js"></script>
<script src="/assets/music/library/songs1-80.js"></script>
<!-- Avatar -->
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<!-- Asset Manager -->
<script src="/assets/assetManager.js"></script>
<script src="/assets/presets.js"></script>
```
### Rendering a Background
```javascript
// Get canvas context
const ctx = canvas.getContext('2d');
// Render background (animated)
function render(time) {
BackgroundEngine.render(ctx, 'space', 'nebula', canvas.width, canvas.height, time);
requestAnimationFrame(render);
}
render(0);
```
### Playing Music
```javascript
// Play by mood
MusicEngine.playByMood('Epic', 8); // mood, energy (1-10)
// Play specific song
MusicEngine.play('epic_001');
// Stop music
MusicEngine.stop();
// Set volume
MusicEngine.setVolume(0.7); // 0-1
```
### Rendering an Avatar
```javascript
// Define avatar configuration
const avatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59'
};
// Render avatar
AvatarSystem.render(ctx, avatar, 'FULL_BODY', 'idle', animationTime);
// With context (vehicle/costume)
ContextRenderer.render(ctx, 'spaceship-cockpit', avatar, x, y, 'idle', time);
```
### Using Presets
```javascript
// Get a preset for a game style
const preset = AssetPresets.getByStyle('action')[0];
// Apply preset
const config = {
music: preset.music,
background: preset.background,
avatarContext: preset.context
};
```
### Using the Recommendation Engine
```javascript
// Get recommendations based on game description
const recommendations = AssetManager.recommend(
"A fast-paced space shooter with epic battles",
"action"
);
// Returns: { background, music, context, preset }
```
## Game Templates
Two HTML templates are provided:
### gameTemplateAssets.html
Full-featured template with:
- Asset library integration
- Loading screen
- Pause menu
- Input handling (keyboard + touch)
- Score/health tracking
Best for: Games using avatars, procedural backgrounds, and music.
### gameTemplateSimple.html
Minimal template with:
- Basic Canvas setup
- Simple game loop
- No external dependencies
Best for: Puzzle games, simple arcade games, creative tools.
## Examples
See `/examples/` for complete working games:
- **platformer-example.html** - FULL_BODY avatar, nature background
- **flappy-example.html** - HEAD_ONLY avatar, sky background
- **racer-example.html** - Vehicle context, urban background
- **comparison-demo.html** - Same game with 4 different asset sets
## Interactive Browser
Visit `/asset-browser.html` for an interactive tool to:
- Browse all 120 backgrounds with live previews
- Preview all 160 music tracks
- Test avatar configurations
- Explore all 26 game contexts
- Browse 220 presets
- Generate random combinations
## Avatar Modes
| Mode | Dimensions | Use Case |
|------|------------|----------|
| HEAD_ONLY | 64×64 | Cockpit views, maze games |
| HEAD_AND_SHOULDERS | 64×96 | Driving, seated vehicles |
| UPPER_BODY | 64×128 | Hoverboard, jetpack games |
| FULL_BODY | 64×160 | Platformers, RPGs, sports |
## Music Moods
| Mood | Energy Range | Best For |
|------|--------------|----------|
| Epic | 7-10 | Boss battles, action |
| Chill | 2-4 | Puzzle, casual |
| Intense | 8-10 | Racing, shooters |
| Playful | 5-7 | Kids games, casual |
| Mysterious | 3-6 | Horror, exploration |
| Heroic | 6-8 | Adventure, platformers |
| Quirky | 4-7 | Puzzle, comedy |
| Ambient | 1-3 | Meditation, creative |
## Background Themes
15 themes, each with 8 variants:
- **space**: nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole
- **nature**: forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow
- **urban**: citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley
- **fantasy**: castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower
- **abstract**: geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix
- **retro**: pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave
- **indoor**: laboratory, classroom, bedroom, kitchen, arcade, office, library, gym
- **weather**: rain, snow, storm, sunset, sunrise, fog, lightning, aurora
- **sports**: stadium, raceTrack, court, field, arena, pool, gym, skatepark
- **seasonal**: springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical
- **underwater**: coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure
- **sky**: clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield
- **mechanical**: gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab
- **spooky**: hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows
- **colorful**: rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism
## Game Contexts (26)
### Vehicles (12)
spaceship-cockpit, race-car, airplane, flappy-style, tank, pacman-style, hoverboard, jetpack, mech-suit, submarine, dragon-rider, motorcycle
### Costumes (10)
knight-armor, space-suit, ninja-outfit, wizard-robes, superhero-cape, athlete-uniform, pirate-outfit, scientist-labcoat, chef-outfit, cowboy-gear
### Pure (4)
platformer-standard, sports-player, adventure-hero, runner
## Documentation
See `/docs/` for detailed documentation:
- **ASSET_LIBRARY_OVERVIEW.md** - Complete system overview
- **GAME_GENERATION_GUIDE.md** - AI generation instructions
- **ADDING_NEW_ASSETS.md** - How to extend the library
- **API_REFERENCE.md** - Full API documentation
## Browser Support
- Chrome 80+
- Firefox 78+
- Safari 14+
- Edge 80+
Requires Canvas 2D and Web Audio API support.
## Performance Notes
- All assets are procedurally generated (no image loading)
- Backgrounds animate at 60fps
- Music uses Web Audio synthesis (low memory)
- Avatar rendering is optimized for multiple instances
- Use `requestAnimationFrame` for smooth animation
## License
Proprietary - GamerComp.com
+354
View File
@@ -0,0 +1,354 @@
(function() {
'use strict';
// =========================================================================
// GAME STYLES (from GamerComp)
// =========================================================================
var GAME_STYLES = [
{
id: 'action-arcade',
name: 'Action/Arcade',
description: 'Space shooters, platformers, fast-paced action',
icon: '🎮',
keywords: ['space', 'shooter', 'platformer', 'action', 'arcade', 'fast'],
recommendedMoods: ['Epic', 'Intense', 'Heroic'],
recommendedThemes: ['space', 'retro', 'urban'],
recommendedContexts: ['spaceship-cockpit', 'platformer-standard', 'jetpack']
},
{
id: 'puzzle',
name: 'Puzzle',
description: 'Match-3, sliding puzzles, brain teasers',
icon: '🧩',
keywords: ['puzzle', 'match', 'brain', 'logic', 'thinking'],
recommendedMoods: ['Chill', 'Ambient', 'Playful'],
recommendedThemes: ['abstract', 'colorful', 'indoor'],
recommendedContexts: ['platformer-standard', 'scientist-labcoat']
},
{
id: 'story-adventure',
name: 'Story Adventure',
description: 'Visual novels, choose-your-adventure',
icon: '📖',
keywords: ['story', 'adventure', 'narrative', 'choice', 'visual'],
recommendedMoods: ['Mysterious', 'Ambient', 'Epic'],
recommendedThemes: ['fantasy', 'nature', 'urban'],
recommendedContexts: ['adventure-hero', 'wizard-robes', 'knight-armor']
},
{
id: 'racing',
name: 'Racing',
description: 'Car racing, running games, speed challenges',
icon: '🏎️',
keywords: ['race', 'racing', 'car', 'speed', 'run', 'fast'],
recommendedMoods: ['Intense', 'Epic', 'Heroic'],
recommendedThemes: ['urban', 'nature', 'sky'],
recommendedContexts: ['race-car', 'motorcycle', 'hoverboard', 'runner']
},
{
id: 'pet-simulator',
name: 'Pet/Simulator',
description: 'Virtual pets, farm games, life simulation',
icon: '🐕',
keywords: ['pet', 'animal', 'farm', 'care', 'virtual', 'simulation'],
recommendedMoods: ['Playful', 'Chill', 'Ambient'],
recommendedThemes: ['nature', 'indoor', 'colorful'],
recommendedContexts: ['platformer-standard', 'chef-outfit']
},
{
id: 'trivia-quiz',
name: 'Trivia/Quiz',
description: 'Quiz shows, fact games, knowledge tests',
icon: '❓',
keywords: ['trivia', 'quiz', 'question', 'knowledge', 'fact'],
recommendedMoods: ['Playful', 'Quirky', 'Ambient'],
recommendedThemes: ['abstract', 'indoor', 'colorful'],
recommendedContexts: ['scientist-labcoat', 'platformer-standard']
},
{
id: 'music-rhythm',
name: 'Music/Rhythm',
description: 'Rhythm games, dance games, music creation',
icon: '🎵',
keywords: ['music', 'rhythm', 'dance', 'beat', 'song'],
recommendedMoods: ['Playful', 'Intense', 'Epic'],
recommendedThemes: ['colorful', 'retro', 'abstract'],
recommendedContexts: ['athlete-uniform', 'superhero-cape']
},
{
id: 'creative-drawing',
name: 'Creative/Drawing',
description: 'Drawing games, design tools, art creation',
icon: '🎨',
keywords: ['draw', 'create', 'art', 'design', 'color', 'paint'],
recommendedMoods: ['Chill', 'Playful', 'Ambient'],
recommendedThemes: ['colorful', 'abstract', 'nature'],
recommendedContexts: ['platformer-standard', 'chef-outfit']
},
{
id: 'rpg-battle',
name: 'RPG/Battle',
description: 'Turn-based battles, dungeon crawlers, adventures',
icon: '⚔️',
keywords: ['rpg', 'battle', 'dungeon', 'quest', 'hero', 'monster'],
recommendedMoods: ['Epic', 'Heroic', 'Mysterious'],
recommendedThemes: ['fantasy', 'spooky', 'nature'],
recommendedContexts: ['knight-armor', 'wizard-robes', 'ninja-outfit', 'dragon-rider']
},
{
id: 'sports',
name: 'Sports',
description: 'Ball games, olympic events, athletic challenges',
icon: '⚽',
keywords: ['sports', 'ball', 'soccer', 'basketball', 'athletic'],
recommendedMoods: ['Intense', 'Epic', 'Heroic'],
recommendedThemes: ['sports', 'nature', 'urban'],
recommendedContexts: ['athlete-uniform', 'sports-player']
},
{
id: 'physics-pinball',
name: 'Physics/Pinball',
description: 'Pinball, angry birds style, physics puzzles',
icon: '🎱',
keywords: ['physics', 'pinball', 'bounce', 'launch', 'gravity'],
recommendedMoods: ['Playful', 'Quirky', 'Intense'],
recommendedThemes: ['mechanical', 'colorful', 'retro'],
recommendedContexts: ['platformer-standard', 'mech-suit']
}
];
// =========================================================================
// MOOD TO THEME MAPPINGS
// =========================================================================
var MOOD_THEME_AFFINITIES = {
Epic: ['space', 'fantasy', 'sports', 'sky'],
Chill: ['nature', 'underwater', 'indoor', 'seasonal'],
Intense: ['urban', 'mechanical', 'sports', 'weather'],
Playful: ['colorful', 'retro', 'nature', 'indoor'],
Mysterious: ['spooky', 'fantasy', 'underwater', 'weather'],
Heroic: ['space', 'fantasy', 'urban', 'sky'],
Quirky: ['retro', 'colorful', 'abstract', 'indoor'],
Ambient: ['nature', 'underwater', 'sky', 'seasonal']
};
// =========================================================================
// THEME TO CONTEXT MAPPINGS
// =========================================================================
var THEME_CONTEXT_AFFINITIES = {
space: ['spaceship-cockpit', 'space-suit', 'mech-suit', 'jetpack'],
nature: ['platformer-standard', 'adventure-hero', 'cowboy-gear'],
urban: ['race-car', 'motorcycle', 'runner', 'superhero-cape'],
fantasy: ['knight-armor', 'wizard-robes', 'dragon-rider'],
spooky: ['ninja-outfit', 'wizard-robes', 'platformer-standard'],
underwater: ['submarine', 'space-suit', 'adventure-hero'],
retro: ['pacman-style', 'platformer-standard', 'athlete-uniform'],
sports: ['athlete-uniform', 'sports-player', 'runner'],
sky: ['airplane', 'flappy-style', 'jetpack', 'dragon-rider'],
colorful: ['superhero-cape', 'athlete-uniform', 'platformer-standard'],
mechanical: ['mech-suit', 'tank', 'scientist-labcoat'],
indoor: ['chef-outfit', 'scientist-labcoat', 'platformer-standard'],
weather: ['adventure-hero', 'cowboy-gear', 'pirate-outfit'],
seasonal: ['platformer-standard', 'athlete-uniform', 'chef-outfit'],
abstract: ['platformer-standard', 'scientist-labcoat']
};
// =========================================================================
// COLOR PALETTES
// =========================================================================
var COLOR_PALETTES = {
'space-blue': ['#0a0a2e', '#1a1a4e', '#2a2a6e', '#4a4a9e', '#6a6abe'],
'forest-green': ['#1a3a1a', '#2a4a2a', '#3a5a3a', '#4a7a4a', '#5a9a5a'],
'sunset-orange': ['#2a1a0a', '#4a2a1a', '#6a3a2a', '#9a5a3a', '#ca7a4a'],
'ocean-teal': ['#0a2a2a', '#1a3a4a', '#2a4a5a', '#3a6a7a', '#4a8a9a'],
'neon-pink': ['#2a0a2a', '#4a1a4a', '#6a2a6a', '#9a3a9a', '#ca4aca'],
'retro-arcade': ['#0a0a0a', '#1a1a3a', '#3a3a6a', '#6a6a9a', '#9a9aca'],
'fantasy-purple': ['#1a0a2a', '#2a1a4a', '#4a2a6a', '#6a3a9a', '#8a4aca'],
'spooky-grey': ['#0a0a0a', '#1a1a1a', '#2a2a2a', '#3a3a3a', '#4a4a4a'],
'sports-red': ['#2a0a0a', '#4a1a1a', '#6a2a2a', '#9a3a3a', '#ca4a4a'],
'nature-brown': ['#2a1a0a', '#3a2a1a', '#4a3a2a', '#5a4a3a', '#6a5a4a'],
'candy-rainbow': ['#ff6b6b', '#ffd93d', '#6bcf7f', '#4ecdc4', '#a06cd5']
};
// =========================================================================
// CATALOG QUERIES
// =========================================================================
function getGameStyles() {
return GAME_STYLES.slice();
}
function getGameStyleById(id) {
return GAME_STYLES.find(function(s) { return s.id === id; }) || null;
}
function getGameStyleByKeyword(keyword) {
keyword = keyword.toLowerCase();
return GAME_STYLES.filter(function(s) {
return s.keywords.some(function(k) {
return k.includes(keyword) || keyword.includes(k);
});
});
}
function getMoodsForTheme(theme) {
var moods = [];
for (var mood in MOOD_THEME_AFFINITIES) {
if (MOOD_THEME_AFFINITIES[mood].includes(theme)) {
moods.push(mood);
}
}
return moods;
}
function getThemesForMood(mood) {
return MOOD_THEME_AFFINITIES[mood] || [];
}
function getContextsForTheme(theme) {
return THEME_CONTEXT_AFFINITIES[theme] || [];
}
function getColorPalette(id) {
return COLOR_PALETTES[id] || COLOR_PALETTES['space-blue'];
}
function getAllColorPalettes() {
return Object.keys(COLOR_PALETTES).map(function(id) {
return { id: id, colors: COLOR_PALETTES[id] };
});
}
function suggestPalette(theme, mood) {
// Theme-based suggestions
var themeMap = {
space: 'space-blue',
nature: 'forest-green',
urban: 'neon-pink',
fantasy: 'fantasy-purple',
spooky: 'spooky-grey',
underwater: 'ocean-teal',
retro: 'retro-arcade',
sports: 'sports-red',
sky: 'space-blue',
colorful: 'candy-rainbow',
mechanical: 'retro-arcade',
indoor: 'nature-brown',
weather: 'sunset-orange',
seasonal: 'forest-green'
};
return themeMap[theme] || 'space-blue';
}
// =========================================================================
// UNIFIED SEARCH
// =========================================================================
function search(query) {
query = query.toLowerCase();
var results = {
gameStyles: [],
moods: [],
themes: [],
contexts: [],
palettes: []
};
// Search game styles
GAME_STYLES.forEach(function(style) {
if (style.name.toLowerCase().includes(query) ||
style.description.toLowerCase().includes(query)) {
results.gameStyles.push(style);
}
});
// Search moods
Object.keys(MOOD_THEME_AFFINITIES).forEach(function(mood) {
if (mood.toLowerCase().includes(query)) {
results.moods.push(mood);
}
});
// Search themes
Object.keys(THEME_CONTEXT_AFFINITIES).forEach(function(theme) {
if (theme.includes(query)) {
results.themes.push(theme);
}
});
// Search palettes
Object.keys(COLOR_PALETTES).forEach(function(palette) {
if (palette.includes(query)) {
results.palettes.push(palette);
}
});
return results;
}
// =========================================================================
// STATISTICS
// =========================================================================
function getStats() {
var musicCount = 0, bgCount = 0, contextCount = 0;
if (window.MusicEngine) {
musicCount = MusicEngine.getCatalog ? MusicEngine.getCatalog().length : 160;
}
if (window.BackgroundEngine) {
bgCount = BackgroundEngine.getTotalCount ? BackgroundEngine.getTotalCount() : 120;
}
if (window.ContextCatalog) {
contextCount = ContextCatalog.getAll ? ContextCatalog.getAll().length : 26;
}
return {
gameStyles: GAME_STYLES.length,
musicSongs: musicCount,
backgrounds: bgCount,
avatarContexts: contextCount,
colorPalettes: Object.keys(COLOR_PALETTES).length,
moods: Object.keys(MOOD_THEME_AFFINITIES).length,
themes: Object.keys(THEME_CONTEXT_AFFINITIES).length
};
}
// =========================================================================
// PUBLIC API
// =========================================================================
window.AssetCatalog = {
// Game styles
GAME_STYLES: GAME_STYLES,
getGameStyles: getGameStyles,
getGameStyleById: getGameStyleById,
getGameStyleByKeyword: getGameStyleByKeyword,
// Mood/Theme mappings
getMoodsForTheme: getMoodsForTheme,
getThemesForMood: getThemesForMood,
getContextsForTheme: getContextsForTheme,
// Color palettes
getColorPalette: getColorPalette,
getAllColorPalettes: getAllColorPalettes,
suggestPalette: suggestPalette,
// Search
search: search,
// Stats
getStats: getStats,
// Direct data access
MOOD_THEME_AFFINITIES: MOOD_THEME_AFFINITIES,
THEME_CONTEXT_AFFINITIES: THEME_CONTEXT_AFFINITIES,
COLOR_PALETTES: COLOR_PALETTES
};
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,762 @@
/**
* Avatar Accessories System
* Defines all unlockable accessories for the avatar customization system
*/
(function() {
'use strict';
// =============================================================================
// RARITY COLORS
// =============================================================================
var RARITY_COLORS = {
common: '#9d9d9d',
uncommon: '#1eff00',
rare: '#0070dd',
epic: '#a335ee',
legendary: '#ff8000',
mythic: '#e6cc80'
};
// =============================================================================
// EYEWEAR (15 items)
// =============================================================================
var EYEWEAR = [
{
id: 'sunglasses-classic',
name: 'Classic Sunglasses',
category: 'eyewear',
unlockLevel: 5,
rarity: 'common',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.0,
hasEffect: false,
description: 'Timeless shades for any occasion'
},
{
id: 'aviator',
name: 'Aviator Glasses',
category: 'eyewear',
unlockLevel: 10,
rarity: 'common',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -1 },
scale: 1.0,
hasEffect: false,
description: 'Top Gun approved eyewear'
},
{
id: 'nerd-glasses',
name: 'Nerd Glasses',
category: 'eyewear',
unlockLevel: 15,
rarity: 'common',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: 0 },
scale: 1.0,
hasEffect: false,
description: 'Intelligence +100 (cosmetic only)'
},
{
id: '3d-glasses',
name: '3D Glasses',
category: 'eyewear',
unlockLevel: 20,
rarity: 'uncommon',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -1 },
scale: 1.0,
hasEffect: false,
description: 'See the world in a whole new dimension'
},
{
id: 'goggles',
name: 'Goggles',
category: 'eyewear',
unlockLevel: 25,
rarity: 'uncommon',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -3 },
scale: 1.1,
hasEffect: false,
description: 'Ready for adventure or science experiments'
},
{
id: 'monocle',
name: 'Monocle',
category: 'eyewear',
unlockLevel: 30,
rarity: 'uncommon',
zIndex: 10,
anchor: 'eyes',
offset: { x: 8, y: 0 },
scale: 0.8,
hasEffect: false,
description: 'Distinguished and sophisticated'
},
{
id: 'star-glasses',
name: 'Star Glasses',
category: 'eyewear',
unlockLevel: 35,
rarity: 'rare',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: false,
description: 'You are a star!'
},
{
id: 'heart-glasses',
name: 'Heart Glasses',
category: 'eyewear',
unlockLevel: 40,
rarity: 'rare',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: false,
description: 'Spread the love wherever you go'
},
{
id: 'sport-visor',
name: 'Sport Visor',
category: 'eyewear',
unlockLevel: 45,
rarity: 'rare',
zIndex: 10,
anchor: 'eyes',
offset: { x: 0, y: -5 },
scale: 1.2,
hasEffect: false,
description: 'Professional gamer gear'
},
{
id: 'vr-headset',
name: 'VR Headset',
category: 'eyewear',
unlockLevel: 50,
rarity: 'epic',
zIndex: 11,
anchor: 'eyes',
offset: { x: 0, y: -4 },
scale: 1.3,
hasEffect: false,
description: 'Immerse yourself in virtual worlds'
},
{
id: 'cyber-visor',
name: 'Cyber Visor',
category: 'eyewear',
unlockLevel: 55,
rarity: 'epic',
zIndex: 11,
anchor: 'eyes',
offset: { x: 0, y: -3 },
scale: 1.2,
hasEffect: false,
description: 'Sleek futuristic eye protection'
},
{
id: 'x-ray-specs',
name: 'X-Ray Specs',
category: 'eyewear',
unlockLevel: 60,
rarity: 'epic',
zIndex: 11,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: true,
description: 'See through walls... probably'
},
{
id: 'laser-eyes',
name: 'Laser Eyes',
category: 'eyewear',
unlockLevel: 70,
rarity: 'legendary',
zIndex: 12,
anchor: 'eyes',
offset: { x: 0, y: 0 },
scale: 1.0,
hasEffect: true,
description: 'Pew pew! Lasers shoot from your eyes'
},
{
id: 'diamond-glasses',
name: 'Diamond Glasses',
category: 'eyewear',
unlockLevel: 80,
rarity: 'legendary',
zIndex: 12,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.1,
hasEffect: false,
description: 'Pure crystallized luxury'
},
{
id: 'cosmic-lenses',
name: 'Cosmic Lenses',
category: 'eyewear',
unlockLevel: 90,
rarity: 'mythic',
zIndex: 13,
anchor: 'eyes',
offset: { x: 0, y: -2 },
scale: 1.2,
hasEffect: true,
description: 'Gaze upon the infinite universe'
}
];
// =============================================================================
// HEADWEAR (20 items)
// =============================================================================
var HEADWEAR = [
{
id: 'baseball-cap',
name: 'Baseball Cap',
category: 'headwear',
unlockLevel: 3,
rarity: 'common',
zIndex: 8,
anchor: 'top',
offset: { x: 0, y: -15 },
scale: 1.0,
hasEffect: false,
description: 'A classic casual cap'
},
{
id: 'backwards-cap',
name: 'Backwards Cap',
category: 'headwear',
unlockLevel: 8,
rarity: 'common',
zIndex: 8,
anchor: 'top',
offset: { x: 0, y: -15 },
scale: 1.0,
hasEffect: false,
description: 'Cool kids wear it backwards'
},
{
id: 'beanie',
name: 'Beanie',
category: 'headwear',
unlockLevel: 12,
rarity: 'common',
zIndex: 8,
anchor: 'top',
offset: { x: 0, y: -12 },
scale: 1.0,
hasEffect: false,
description: 'Cozy knitted headwear'
},
{
id: 'party-hat',
name: 'Party Hat',
category: 'headwear',
unlockLevel: 18,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -20 },
scale: 1.0,
hasEffect: false,
description: 'Every day is a celebration!'
},
{
id: 'chef-hat',
name: 'Chef Hat',
category: 'headwear',
unlockLevel: 22,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -25 },
scale: 1.1,
hasEffect: false,
description: 'Master of the kitchen'
},
{
id: 'crown-bronze',
name: 'Bronze Crown',
category: 'headwear',
unlockLevel: 25,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.0,
hasEffect: false,
description: 'A humble crown for rising royalty'
},
{
id: 'hard-hat',
name: 'Hard Hat',
category: 'headwear',
unlockLevel: 26,
rarity: 'uncommon',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -14 },
scale: 1.0,
hasEffect: false,
description: 'Safety first on the job site'
},
{
id: 'wizard-hat',
name: 'Wizard Hat',
category: 'headwear',
unlockLevel: 30,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -30 },
scale: 1.2,
hasEffect: false,
description: 'Channel your inner sorcerer'
},
{
id: 'pirate-hat',
name: 'Pirate Hat',
category: 'headwear',
unlockLevel: 35,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.1,
hasEffect: false,
description: 'Arrr! Set sail for adventure'
},
{
id: 'santa-hat',
name: 'Santa Hat',
category: 'headwear',
unlockLevel: 38,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 3, y: -16 },
scale: 1.0,
hasEffect: false,
description: 'Ho ho ho! Spread holiday cheer'
},
{
id: 'crown-silver',
name: 'Silver Crown',
category: 'headwear',
unlockLevel: 40,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.0,
hasEffect: false,
description: 'A noble crown for dedicated players'
},
{
id: 'graduation-cap',
name: 'Graduation Cap',
category: 'headwear',
unlockLevel: 42,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -16 },
scale: 1.0,
hasEffect: false,
description: 'Scholar of the gaming arts'
},
{
id: 'bunny-ears',
name: 'Bunny Ears',
category: 'headwear',
unlockLevel: 45,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -22 },
scale: 1.1,
hasEffect: false,
description: 'Hop into the fun!'
},
{
id: 'devil-horns',
name: 'Devil Horns',
category: 'headwear',
unlockLevel: 48,
rarity: 'rare',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -14 },
scale: 1.0,
hasEffect: false,
description: 'A little mischief never hurt anyone'
},
{
id: 'halo',
name: 'Halo',
category: 'headwear',
unlockLevel: 52,
rarity: 'epic',
zIndex: 7,
anchor: 'top',
offset: { x: 0, y: -25 },
scale: 1.0,
hasEffect: true,
description: 'An angelic golden ring of light'
},
{
id: 'top-hat',
name: 'Top Hat',
category: 'headwear',
unlockLevel: 56,
rarity: 'epic',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -28 },
scale: 1.1,
hasEffect: false,
description: 'Dapper and distinguished'
},
{
id: 'crown-gold',
name: 'Gold Crown',
category: 'headwear',
unlockLevel: 60,
rarity: 'epic',
zIndex: 10,
anchor: 'top',
offset: { x: 0, y: -18 },
scale: 1.1,
hasEffect: false,
description: 'True royalty in golden splendor'
},
{
id: 'viking-helmet',
name: 'Viking Helmet',
category: 'headwear',
unlockLevel: 62,
rarity: 'epic',
zIndex: 9,
anchor: 'top',
offset: { x: 0, y: -16 },
scale: 1.2,
hasEffect: false,
description: 'Conquer games like a Norse warrior'
},
{
id: 'crown-diamond',
name: 'Diamond Crown',
category: 'headwear',
unlockLevel: 80,
rarity: 'legendary',
zIndex: 10,
anchor: 'top',
offset: { x: 0, y: -20 },
scale: 1.2,
hasEffect: true,
description: 'A magnificent crown of pure diamonds'
},
{
id: 'crown-cosmic',
name: 'Cosmic Crown',
category: 'headwear',
unlockLevel: 100,
rarity: 'mythic',
zIndex: 11,
anchor: 'top',
offset: { x: 0, y: -22 },
scale: 1.3,
hasEffect: true,
description: 'The ultimate crown, forged from stardust'
}
];
// =============================================================================
// EFFECTS/AURAS (10 items)
// =============================================================================
var EFFECTS = [
{
id: 'sparkle-trail',
name: 'Sparkle Trail',
category: 'effect',
unlockLevel: 15,
rarity: 'uncommon',
zIndex: 1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.0,
hasEffect: true,
effectType: 'particles',
color: '#FFD700',
description: 'Leave a trail of golden sparkles'
},
{
id: 'fire-aura',
name: 'Fire Aura',
category: 'effect',
unlockLevel: 25,
rarity: 'rare',
zIndex: 0,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.2,
hasEffect: true,
effectType: 'glow',
color: '#FF4500',
description: 'Surrounded by fierce flames'
},
{
id: 'lightning-crackle',
name: 'Lightning Crackle',
category: 'effect',
unlockLevel: 35,
rarity: 'rare',
zIndex: 2,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.1,
hasEffect: true,
effectType: 'electric',
color: '#00BFFF',
description: 'Electricity crackles around you'
},
{
id: 'rainbow-glow',
name: 'Rainbow Glow',
category: 'effect',
unlockLevel: 45,
rarity: 'epic',
zIndex: 0,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.3,
hasEffect: true,
effectType: 'glow',
color: 'rainbow',
description: 'Radiate all colors of the spectrum'
},
{
id: 'star-particles',
name: 'Star Particles',
category: 'effect',
unlockLevel: 55,
rarity: 'epic',
zIndex: 2,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.2,
hasEffect: true,
effectType: 'particles',
color: '#FFFF00',
description: 'Tiny stars orbit around you'
},
{
id: 'ice-crystals',
name: 'Ice Crystals',
category: 'effect',
unlockLevel: 65,
rarity: 'legendary',
zIndex: 2,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.2,
hasEffect: true,
effectType: 'particles',
color: '#87CEEB',
description: 'Frozen crystals shimmer around you'
},
{
id: 'shadow-effect',
name: 'Shadow Effect',
category: 'effect',
unlockLevel: 70,
rarity: 'legendary',
zIndex: -1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.4,
hasEffect: true,
effectType: 'shadow',
color: '#2F2F4F',
description: 'Darkness follows your every move'
},
{
id: 'golden-glow',
name: 'Golden Glow',
category: 'effect',
unlockLevel: 75,
rarity: 'legendary',
zIndex: 0,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.3,
hasEffect: true,
effectType: 'glow',
color: '#FFD700',
description: 'Bathe in pure golden light'
},
{
id: 'void-aura',
name: 'Void Aura',
category: 'effect',
unlockLevel: 85,
rarity: 'mythic',
zIndex: -1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.5,
hasEffect: true,
effectType: 'void',
color: '#4B0082',
description: 'The void itself surrounds you'
},
{
id: 'cosmic-trail',
name: 'Cosmic Trail',
category: 'effect',
unlockLevel: 95,
rarity: 'mythic',
zIndex: 1,
anchor: 'head',
offset: { x: 0, y: 0 },
scale: 1.4,
hasEffect: true,
effectType: 'cosmic',
color: 'cosmic',
description: 'Trail galaxies and nebulae in your wake'
}
];
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Get all accessories combined into a single array
*/
function getAll() {
return [].concat(EYEWEAR, HEADWEAR, EFFECTS);
}
/**
* Get accessories filtered by category
* @param {string} category - 'eyewear', 'headwear', or 'effect'
*/
function getByCategory(category) {
switch (category) {
case 'eyewear':
return EYEWEAR.slice();
case 'headwear':
return HEADWEAR.slice();
case 'effect':
return EFFECTS.slice();
default:
return [];
}
}
/**
* Find an accessory by its unique ID
* @param {string} id - The accessory ID
*/
function getById(id) {
var all = getAll();
for (var i = 0; i < all.length; i++) {
if (all[i].id === id) {
return all[i];
}
}
return null;
}
/**
* Get all accessories of a specific rarity
* @param {string} rarity - 'common', 'uncommon', 'rare', 'epic', 'legendary', or 'mythic'
*/
function getByRarity(rarity) {
return getAll().filter(function(item) {
return item.rarity === rarity;
});
}
/**
* Get all accessories unlockable at or before the given level
* @param {number} level - The player's current level
*/
function getUnlockedAtLevel(level) {
return getAll().filter(function(item) {
return item.unlockLevel <= level;
});
}
/**
* Get accessories that unlock exactly at the specified level
* @param {number} level - The level to check
*/
function getNewlyUnlockedAtLevel(level) {
return getAll().filter(function(item) {
return item.unlockLevel === level;
});
}
/**
* Get the color associated with a rarity
* @param {string} rarity - The rarity level
*/
function getRarityColor(rarity) {
return RARITY_COLORS[rarity] || RARITY_COLORS.common;
}
/**
* Get an organized catalog for UI display
* Returns accessories grouped by category and sorted by unlock level
*/
function getCatalog() {
var sortByLevel = function(a, b) {
return a.unlockLevel - b.unlockLevel;
};
return {
eyewear: EYEWEAR.slice().sort(sortByLevel),
headwear: HEADWEAR.slice().sort(sortByLevel),
effects: EFFECTS.slice().sort(sortByLevel),
totalCount: EYEWEAR.length + HEADWEAR.length + EFFECTS.length,
rarityColors: Object.assign({}, RARITY_COLORS)
};
}
// =============================================================================
// PUBLIC API
// =============================================================================
window.AvatarAccessories = {
// Data arrays
EYEWEAR: EYEWEAR,
HEADWEAR: HEADWEAR,
EFFECTS: EFFECTS,
RARITY_COLORS: RARITY_COLORS,
// Methods
getAll: getAll,
getByCategory: getByCategory,
getById: getById,
getByRarity: getByRarity,
getUnlockedAtLevel: getUnlockedAtLevel,
getNewlyUnlockedAtLevel: getNewlyUnlockedAtLevel,
getRarityColor: getRarityColor,
getCatalog: getCatalog
};
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+473
View File
@@ -0,0 +1,473 @@
(function() {
'use strict';
// Face shapes (8 options)
var FACE_SHAPES = [
{ id: 'round', name: 'Round', widthRatio: 1.0, heightRatio: 1.0, jawCurve: 0.9 },
{ id: 'oval', name: 'Oval', widthRatio: 0.85, heightRatio: 1.1, jawCurve: 0.7 },
{ id: 'square', name: 'Square', widthRatio: 1.0, heightRatio: 0.95, jawCurve: 0.3 },
{ id: 'heart', name: 'Heart', widthRatio: 0.9, heightRatio: 1.05, jawCurve: 0.8, chinPoint: 0.6 },
{ id: 'long', name: 'Long', widthRatio: 0.8, heightRatio: 1.2, jawCurve: 0.5 },
{ id: 'wide', name: 'Wide', widthRatio: 1.15, heightRatio: 0.9, jawCurve: 0.6 },
{ id: 'angular', name: 'Angular', widthRatio: 0.95, heightRatio: 1.0, jawCurve: 0.2, cheekbones: 0.7 },
{ id: 'soft', name: 'Soft', widthRatio: 1.05, heightRatio: 1.0, jawCurve: 0.95 }
];
// Skin tones (20 options - full diversity)
var SKIN_TONES = [
{ id: 'fair1', hex: '#FFECD1', name: 'Porcelain' },
{ id: 'fair2', hex: '#FFE4C4', name: 'Ivory' },
{ id: 'fair3', hex: '#FFDAB9', name: 'Peach' },
{ id: 'fair4', hex: '#FFD5B5', name: 'Cream' },
{ id: 'light1', hex: '#F5D0A9', name: 'Light Beige' },
{ id: 'light2', hex: '#E8C49A', name: 'Warm Beige' },
{ id: 'light3', hex: '#DEB887', name: 'Sand' },
{ id: 'medium1', hex: '#D4A373', name: 'Golden' },
{ id: 'medium2', hex: '#C9A06A', name: 'Honey' },
{ id: 'medium3', hex: '#BC8F5F', name: 'Caramel' },
{ id: 'medium4', hex: '#B07D4B', name: 'Toffee' },
{ id: 'tan1', hex: '#A67B5B', name: 'Tan' },
{ id: 'tan2', hex: '#996B4D', name: 'Bronze' },
{ id: 'tan3', hex: '#8B6D4C', name: 'Cinnamon' },
{ id: 'brown1', hex: '#7D5A3C', name: 'Chestnut' },
{ id: 'brown2', hex: '#6F4E37', name: 'Cocoa' },
{ id: 'brown3', hex: '#5D4532', name: 'Espresso' },
{ id: 'dark1', hex: '#4E3B2D', name: 'Mocha' },
{ id: 'dark2', hex: '#3D2B1F', name: 'Ebony' },
{ id: 'dark3', hex: '#2C1F15', name: 'Onyx' }
];
// Eye shapes (12 options)
var EYE_SHAPES = [
{ id: 'round', name: 'Round', width: 0.15, height: 0.12, curve: 1.0 },
{ id: 'almond', name: 'Almond', width: 0.18, height: 0.08, curve: 0.6 },
{ id: 'oval', name: 'Oval', width: 0.16, height: 0.10, curve: 0.8 },
{ id: 'narrow', name: 'Narrow', width: 0.20, height: 0.06, curve: 0.4 },
{ id: 'wide', name: 'Wide', width: 0.19, height: 0.11, curve: 0.7 },
{ id: 'droopy', name: 'Droopy', width: 0.17, height: 0.09, curve: 0.5, droop: 0.3 },
{ id: 'upturned', name: 'Upturned', width: 0.17, height: 0.09, curve: 0.6, upturn: 0.3 },
{ id: 'cat', name: 'Cat', width: 0.18, height: 0.07, curve: 0.4, upturn: 0.5 },
{ id: 'sleepy', name: 'Sleepy', width: 0.16, height: 0.07, curve: 0.5, lidDroop: 0.4 },
{ id: 'surprised', name: 'Surprised', width: 0.16, height: 0.14, curve: 1.0 },
{ id: 'angry', name: 'Angry', width: 0.17, height: 0.08, curve: 0.5, innerAngle: -0.2 },
{ id: 'happy', name: 'Happy', width: 0.17, height: 0.08, curve: 0.7, squint: 0.3 }
];
// Eye colors (16 options)
var EYE_COLORS = [
{ id: 'brown', hex: '#634e34', name: 'Brown' },
{ id: 'darkBrown', hex: '#3d2314', name: 'Dark Brown' },
{ id: 'hazel', hex: '#8b7355', name: 'Hazel' },
{ id: 'green', hex: '#3d7a3d', name: 'Green' },
{ id: 'blue', hex: '#4682b4', name: 'Blue' },
{ id: 'lightBlue', hex: '#87ceeb', name: 'Light Blue' },
{ id: 'gray', hex: '#708090', name: 'Gray' },
{ id: 'amber', hex: '#b8860b', name: 'Amber' },
{ id: 'violet', hex: '#8b008b', name: 'Violet' },
{ id: 'emerald', hex: '#50c878', name: 'Emerald' },
{ id: 'iceBlue', hex: '#a5d8e6', name: 'Ice Blue' },
{ id: 'gold', hex: '#daa520', name: 'Gold' },
{ id: 'red', hex: '#8b0000', name: 'Red' },
{ id: 'black', hex: '#1a1a1a', name: 'Black' },
{ id: 'honey', hex: '#c9a06a', name: 'Honey' },
{ id: 'turquoise', hex: '#40e0d0', name: 'Turquoise' }
];
// Eyebrow shapes (10 options)
var EYEBROW_SHAPES = [
{ id: 'natural', name: 'Natural', thickness: 0.5, arch: 0.3 },
{ id: 'arched', name: 'Arched', thickness: 0.5, arch: 0.6 },
{ id: 'straight', name: 'Straight', thickness: 0.5, arch: 0.0 },
{ id: 'thick', name: 'Thick', thickness: 0.8, arch: 0.3 },
{ id: 'thin', name: 'Thin', thickness: 0.25, arch: 0.3 },
{ id: 'angry', name: 'Angry', thickness: 0.5, arch: 0.2, innerAngle: -0.4 },
{ id: 'worried', name: 'Worried', thickness: 0.5, arch: 0.4, innerAngle: 0.3 },
{ id: 'bushy', name: 'Bushy', thickness: 0.9, arch: 0.2, wild: true },
{ id: 'sleek', name: 'Sleek', thickness: 0.3, arch: 0.5 },
{ id: 'curved', name: 'Curved', thickness: 0.5, arch: 0.7 }
];
// Nose shapes (8 options)
var NOSE_SHAPES = [
{ id: 'button', name: 'Button', width: 0.12, length: 0.15 },
{ id: 'pointed', name: 'Pointed', width: 0.10, length: 0.22 },
{ id: 'roman', name: 'Roman', width: 0.14, length: 0.25, bridge: 0.6 },
{ id: 'snub', name: 'Snub', width: 0.13, length: 0.12, upturn: 0.4 },
{ id: 'wide', name: 'Wide', width: 0.18, length: 0.18 },
{ id: 'narrow', name: 'Narrow', width: 0.08, length: 0.20 },
{ id: 'aquiline', name: 'Aquiline', width: 0.12, length: 0.24, bridge: 0.8 },
{ id: 'flat', name: 'Flat', width: 0.16, length: 0.10 }
];
// Mouth shapes (10 options)
var MOUTH_SHAPES = [
{ id: 'smile', name: 'Smile', width: 0.25, curve: 0.3 },
{ id: 'neutral', name: 'Neutral', width: 0.22, curve: 0 },
{ id: 'grin', name: 'Grin', width: 0.30, curve: 0.5, showTeeth: true },
{ id: 'smirk', name: 'Smirk', width: 0.22, curve: 0.2, asymmetric: true },
{ id: 'pout', name: 'Pout', width: 0.20, curve: -0.2, lipFullness: 0.8 },
{ id: 'open', name: 'Open', width: 0.24, curve: 0.1, openness: 0.3 },
{ id: 'small', name: 'Small', width: 0.16, curve: 0.1 },
{ id: 'wide', name: 'Wide', width: 0.32, curve: 0.1 },
{ id: 'thin', name: 'Thin', width: 0.24, curve: 0, lipFullness: 0.3 },
{ id: 'full', name: 'Full', width: 0.26, curve: 0.15, lipFullness: 0.9 }
];
// Ear shapes (6 options)
var EAR_SHAPES = [
{ id: 'normal', name: 'Normal', size: 1.0, angle: 0 },
{ id: 'small', name: 'Small', size: 0.7, angle: 0 },
{ id: 'large', name: 'Large', size: 1.3, angle: 0 },
{ id: 'pointed', name: 'Pointed', size: 1.0, angle: 0, pointed: true },
{ id: 'round', name: 'Round', size: 1.0, angle: 0, roundness: 0.9 },
{ id: 'flat', name: 'Flat', size: 0.9, angle: 15 }
];
// Hair styles (30 options)
var HAIR_STYLES = [
{ id: 'short-messy', name: 'Short Messy', length: 'short', coverage: 0.3 },
{ id: 'short-neat', name: 'Short Neat', length: 'short', coverage: 0.25 },
{ id: 'short-curly', name: 'Short Curly', length: 'short', coverage: 0.35 },
{ id: 'medium-straight', name: 'Medium Straight', length: 'medium', coverage: 0.5 },
{ id: 'medium-wavy', name: 'Medium Wavy', length: 'medium', coverage: 0.55 },
{ id: 'long-straight', name: 'Long Straight', length: 'long', coverage: 0.7 },
{ id: 'long-wavy', name: 'Long Wavy', length: 'long', coverage: 0.75 },
{ id: 'ponytail', name: 'Ponytail', length: 'medium', coverage: 0.4 },
{ id: 'pigtails', name: 'Pigtails', length: 'medium', coverage: 0.45 },
{ id: 'bun', name: 'Bun', length: 'long', coverage: 0.35 },
{ id: 'mohawk', name: 'Mohawk', length: 'short', coverage: 0.2 },
{ id: 'afro', name: 'Afro', length: 'medium', coverage: 0.8 },
{ id: 'buzz', name: 'Buzz Cut', length: 'short', coverage: 0.15 },
{ id: 'bald', name: 'Bald', length: 'none', coverage: 0 },
{ id: 'sidepart', name: 'Side Part', length: 'short', coverage: 0.3 },
{ id: 'spiky', name: 'Spiky', length: 'short', coverage: 0.35, spikes: true },
{ id: 'slicked', name: 'Slicked Back', length: 'short', coverage: 0.25 },
{ id: 'bob', name: 'Bob', length: 'medium', coverage: 0.5 },
{ id: 'pixie', name: 'Pixie', length: 'short', coverage: 0.35 },
{ id: 'braids', name: 'Braids', length: 'long', coverage: 0.6 },
{ id: 'dreads', name: 'Dreads', length: 'long', coverage: 0.7 },
{ id: 'undercut', name: 'Undercut', length: 'short', coverage: 0.25, sides: 'shaved' },
{ id: 'fauxhawk', name: 'Faux Hawk', length: 'short', coverage: 0.3 },
{ id: 'mullet', name: 'Mullet', length: 'medium', coverage: 0.45, backLength: 'long' },
{ id: 'bowl', name: 'Bowl Cut', length: 'medium', coverage: 0.55 },
{ id: 'combover', name: 'Comb Over', length: 'short', coverage: 0.2 },
{ id: 'bangs', name: 'Bangs', length: 'medium', coverage: 0.5, bangs: true },
{ id: 'sidesweep', name: 'Side Sweep', length: 'medium', coverage: 0.45 },
{ id: 'topknot', name: 'Top Knot', length: 'long', coverage: 0.3 },
{ id: 'cornrows', name: 'Cornrows', length: 'medium', coverage: 0.4 }
];
// Hair colors (20 options)
var HAIR_COLORS = [
{ id: 'black', hex: '#1a1a1a', name: 'Black' },
{ id: 'darkBrown', hex: '#2c1810', name: 'Dark Brown' },
{ id: 'brown', hex: '#4a3728', name: 'Brown' },
{ id: 'lightBrown', hex: '#7a5a3a', name: 'Light Brown' },
{ id: 'auburn', hex: '#8b4513', name: 'Auburn' },
{ id: 'ginger', hex: '#b5651d', name: 'Ginger' },
{ id: 'strawberry', hex: '#c67171', name: 'Strawberry' },
{ id: 'blonde', hex: '#d4b896', name: 'Blonde' },
{ id: 'platinum', hex: '#e8e4c9', name: 'Platinum' },
{ id: 'white', hex: '#f0f0f0', name: 'White' },
{ id: 'gray', hex: '#808080', name: 'Gray' },
{ id: 'red', hex: '#cc0000', name: 'Red' },
{ id: 'blue', hex: '#0066cc', name: 'Blue' },
{ id: 'purple', hex: '#7b2d8e', name: 'Purple' },
{ id: 'green', hex: '#228b22', name: 'Green' },
{ id: 'pink', hex: '#ff69b4', name: 'Pink' },
{ id: 'teal', hex: '#008b8b', name: 'Teal' },
{ id: 'orange', hex: '#ff6600', name: 'Orange' },
{ id: 'silver', hex: '#c0c0c0', name: 'Silver' },
{ id: 'rainbow', hex: 'linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff)', name: 'Rainbow', isGradient: true }
];
// Facial hair (10 options)
var FACIAL_HAIR = [
{ id: 'none', name: 'None' },
{ id: 'stubble', name: 'Stubble', density: 0.3, length: 0.1 },
{ id: 'goatee', name: 'Goatee', coverage: 'chin', length: 0.4 },
{ id: 'mustache', name: 'Mustache', coverage: 'upper-lip', length: 0.3, style: 'classic' },
{ id: 'fullBeard', name: 'Full Beard', coverage: 'full', length: 0.7 },
{ id: 'shortBeard', name: 'Short Beard', coverage: 'full', length: 0.3 },
{ id: 'soulPatch', name: 'Soul Patch', coverage: 'chin-center', length: 0.3 },
{ id: 'mutton', name: 'Mutton Chops', coverage: 'sides', length: 0.5 },
{ id: 'handlebar', name: 'Handlebar', coverage: 'upper-lip', length: 0.4, style: 'handlebar' },
{ id: 'vandyke', name: 'Van Dyke', coverage: 'chin-mustache', length: 0.5 }
];
// Body types (6 options)
var BODY_TYPES = [
{ id: 'slim', name: 'Slim', widthRatio: 0.8, heightRatio: 1.0 },
{ id: 'average', name: 'Average', widthRatio: 1.0, heightRatio: 1.0 },
{ id: 'athletic', name: 'Athletic', widthRatio: 1.1, heightRatio: 1.0 },
{ id: 'stocky', name: 'Stocky', widthRatio: 1.2, heightRatio: 0.95 },
{ id: 'tall', name: 'Tall', widthRatio: 0.95, heightRatio: 1.15 },
{ id: 'short', name: 'Short', widthRatio: 1.0, heightRatio: 0.85 }
];
// Clothing styles (4 options)
var CLOTHING_STYLES = [
{ id: 'casual', name: 'Casual', hasCollar: false, sleeveLength: 'short' },
{ id: 'sporty', name: 'Sporty', hasCollar: false, sleeveLength: 'short', hasStripes: true },
{ id: 'formal', name: 'Formal', hasCollar: true, sleeveLength: 'long' },
{ id: 'scifi', name: 'Sci-Fi', hasCollar: true, sleeveLength: 'long', hasGlow: true }
];
// Preset colors for clothing
var CLOTHING_COLORS = [
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#1abc9c',
'#3498db', '#9b59b6', '#34495e', '#95a5a6', '#ecf0f1',
'#c0392b', '#d35400', '#f39c12', '#27ae60', '#16a085',
'#2980b9', '#8e44ad', '#2c3e50', '#7f8c8d', '#bdc3c7'
];
// 10 Default guest avatars
var GUEST_AVATARS = [
{
id: 'guest_1',
name: 'Guest Blue',
base: {
faceShape: 'round',
skinTone: 'medium1',
eyes: { shape: 'round', color: 'blue' },
eyebrows: 'natural',
nose: 'button',
mouth: 'smile',
ears: 'normal',
hair: { style: 'short-messy', color: 'darkBrown' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#3498db', pantsColor: '#2c3e50' }
}
},
{
id: 'guest_2',
name: 'Guest Red',
base: {
faceShape: 'oval',
skinTone: 'light2',
eyes: { shape: 'almond', color: 'brown' },
eyebrows: 'arched',
nose: 'pointed',
mouth: 'neutral',
ears: 'normal',
hair: { style: 'long-straight', color: 'black' },
facialHair: 'none',
body: { type: 'slim' },
clothing: { style: 'casual', shirtColor: '#e74c3c', pantsColor: '#34495e' }
}
},
{
id: 'guest_3',
name: 'Guest Green',
base: {
faceShape: 'square',
skinTone: 'tan1',
eyes: { shape: 'narrow', color: 'darkBrown' },
eyebrows: 'thick',
nose: 'wide',
mouth: 'grin',
ears: 'large',
hair: { style: 'buzz', color: 'black' },
facialHair: 'stubble',
body: { type: 'athletic' },
clothing: { style: 'sporty', shirtColor: '#2ecc71', pantsColor: '#2c3e50' }
}
},
{
id: 'guest_4',
name: 'Guest Purple',
base: {
faceShape: 'heart',
skinTone: 'fair2',
eyes: { shape: 'wide', color: 'green' },
eyebrows: 'curved',
nose: 'snub',
mouth: 'small',
ears: 'small',
hair: { style: 'bob', color: 'auburn' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#9b59b6', pantsColor: '#7f8c8d' }
}
},
{
id: 'guest_5',
name: 'Guest Orange',
base: {
faceShape: 'long',
skinTone: 'brown1',
eyes: { shape: 'oval', color: 'amber' },
eyebrows: 'straight',
nose: 'roman',
mouth: 'full',
ears: 'normal',
hair: { style: 'afro', color: 'black' },
facialHair: 'goatee',
body: { type: 'tall' },
clothing: { style: 'casual', shirtColor: '#e67e22', pantsColor: '#34495e' }
}
},
{
id: 'guest_6',
name: 'Guest Teal',
base: {
faceShape: 'soft',
skinTone: 'dark1',
eyes: { shape: 'happy', color: 'darkBrown' },
eyebrows: 'natural',
nose: 'flat',
mouth: 'wide',
ears: 'round',
hair: { style: 'dreads', color: 'black' },
facialHair: 'none',
body: { type: 'stocky' },
clothing: { style: 'casual', shirtColor: '#1abc9c', pantsColor: '#2c3e50' }
}
},
{
id: 'guest_7',
name: 'Guest Gray',
base: {
faceShape: 'angular',
skinTone: 'light3',
eyes: { shape: 'cat', color: 'gray' },
eyebrows: 'sleek',
nose: 'narrow',
mouth: 'smirk',
ears: 'pointed',
hair: { style: 'undercut', color: 'platinum' },
facialHair: 'none',
body: { type: 'slim' },
clothing: { style: 'scifi', shirtColor: '#95a5a6', pantsColor: '#34495e' }
}
},
{
id: 'guest_8',
name: 'Guest Yellow',
base: {
faceShape: 'wide',
skinTone: 'medium3',
eyes: { shape: 'surprised', color: 'hazel' },
eyebrows: 'worried',
nose: 'button',
mouth: 'open',
ears: 'normal',
hair: { style: 'spiky', color: 'blonde' },
facialHair: 'none',
body: { type: 'short' },
clothing: { style: 'casual', shirtColor: '#f1c40f', pantsColor: '#7f8c8d' }
}
},
{
id: 'guest_9',
name: 'Guest Navy',
base: {
faceShape: 'oval',
skinTone: 'tan2',
eyes: { shape: 'droopy', color: 'brown' },
eyebrows: 'bushy',
nose: 'aquiline',
mouth: 'neutral',
ears: 'flat',
hair: { style: 'combover', color: 'gray' },
facialHair: 'fullBeard',
body: { type: 'stocky' },
clothing: { style: 'formal', shirtColor: '#2c3e50', pantsColor: '#1a1a2e' }
}
},
{
id: 'guest_10',
name: 'Guest Pink',
base: {
faceShape: 'round',
skinTone: 'fair3',
eyes: { shape: 'round', color: 'lightBlue' },
eyebrows: 'thin',
nose: 'snub',
mouth: 'pout',
ears: 'small',
hair: { style: 'pigtails', color: 'strawberry' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#ff69b4', pantsColor: '#bdc3c7' }
}
}
];
// Helper function to find item by ID
function findById(array, id) {
for (var i = 0; i < array.length; i++) {
if (array[i].id === id) {
return array[i];
}
}
return null;
}
// Public API
window.AvatarData = {
FACE_SHAPES: FACE_SHAPES,
SKIN_TONES: SKIN_TONES,
EYE_SHAPES: EYE_SHAPES,
EYE_COLORS: EYE_COLORS,
EYEBROW_SHAPES: EYEBROW_SHAPES,
NOSE_SHAPES: NOSE_SHAPES,
MOUTH_SHAPES: MOUTH_SHAPES,
EAR_SHAPES: EAR_SHAPES,
HAIR_STYLES: HAIR_STYLES,
HAIR_COLORS: HAIR_COLORS,
FACIAL_HAIR: FACIAL_HAIR,
BODY_TYPES: BODY_TYPES,
CLOTHING_STYLES: CLOTHING_STYLES,
CLOTHING_COLORS: CLOTHING_COLORS,
GUEST_AVATARS: GUEST_AVATARS,
// Helper methods
getFaceShape: function(id) { return findById(FACE_SHAPES, id); },
getSkinTone: function(id) { return findById(SKIN_TONES, id); },
getEyeShape: function(id) { return findById(EYE_SHAPES, id); },
getEyeColor: function(id) { return findById(EYE_COLORS, id); },
getEyebrowShape: function(id) { return findById(EYEBROW_SHAPES, id); },
getNoseShape: function(id) { return findById(NOSE_SHAPES, id); },
getMouthShape: function(id) { return findById(MOUTH_SHAPES, id); },
getEarShape: function(id) { return findById(EAR_SHAPES, id); },
getHairStyle: function(id) { return findById(HAIR_STYLES, id); },
getHairColor: function(id) { return findById(HAIR_COLORS, id); },
getFacialHair: function(id) { return findById(FACIAL_HAIR, id); },
getBodyType: function(id) { return findById(BODY_TYPES, id); },
getClothingStyle: function(id) { return findById(CLOTHING_STYLES, id); },
getRandomGuestAvatar: function() {
return GUEST_AVATARS[Math.floor(Math.random() * GUEST_AVATARS.length)];
},
createDefaultAvatar: function() {
return {
faceShape: 'round',
skinTone: 'medium1',
eyes: {
shape: 'round',
color: 'brown'
},
eyebrows: 'natural',
nose: 'button',
mouth: 'smile',
ears: 'normal',
hair: {
style: 'short-neat',
color: 'brown'
},
facialHair: 'none',
body: {
type: 'average'
},
clothing: {
style: 'casual',
shirtColor: '#3498db',
pantsColor: '#2c3e50'
}
};
}
};
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,997 @@
/**
* AvatarSystem - Main API for Mii-style avatar system
* Coordinates data, rendering, animations, and accessories
*
* Depends on: AvatarData, AvatarAnimations, AvatarRenderer, AvatarAccessories, AccessoryRenderer
*/
(function() {
'use strict';
// ─── Private State ───────────────────────────────────
// In-memory cache of loaded avatars
var avatarCache = new Map();
// Animation state per avatar
var animationState = new Map();
// ─── Private Helpers ─────────────────────────────────
/**
* Validate avatar data structure
* @param {Object} data - Avatar data to validate
* @returns {boolean} True if valid
*/
function validateAvatar(data) {
if (!data || typeof data !== 'object') return false;
if (!data.base || typeof data.base !== 'object') return false;
// Check required base fields exist
var requiredFields = ['faceShape', 'skinTone', 'eyes', 'eyebrows', 'nose', 'mouth', 'ears', 'hair', 'body', 'clothing'];
for (var i = 0; i < requiredFields.length; i++) {
if (data.base[requiredFields[i]] === undefined) {
return false;
}
}
// Check nested objects
if (!data.base.eyes || typeof data.base.eyes !== 'object') return false;
if (!data.base.eyes.shape || !data.base.eyes.color) return false;
if (!data.base.hair || typeof data.base.hair !== 'object') return false;
if (!data.base.hair.style || !data.base.hair.color) return false;
if (!data.base.body || typeof data.base.body !== 'object') return false;
if (!data.base.body.type) return false;
if (!data.base.clothing || typeof data.base.clothing !== 'object') return false;
if (!data.base.clothing.style || !data.base.clothing.shirtColor || !data.base.clothing.pantsColor) return false;
return true;
}
/**
* Create a default avatar with standard settings
* @returns {Object} Default avatar object
*/
function createDefault() {
return {
userId: null,
base: {
faceShape: 'oval',
skinTone: '#d4a373',
eyes: { shape: 'round', color: 'brown' },
eyebrows: 'natural',
nose: 'button',
mouth: 'smile',
ears: 'normal',
hair: { style: 'short-messy', color: '#2c1810' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#3498db', pantsColor: '#2c3e50' }
},
earned: { eyewear: [], headwear: [], effects: [] },
equipped: { eyewear: null, headwear: null, effect: null },
level: 1,
xp: 0
};
}
/**
* Deep clone an avatar object
* @param {Object} avatar - Avatar to clone
* @returns {Object} Cloned avatar
*/
function cloneAvatar(avatar) {
return JSON.parse(JSON.stringify(avatar));
}
/**
* Merge customization into base avatar
* @param {Object} target - Target avatar base
* @param {Object} source - Source customization
* @returns {Object} Merged base object
*/
function deepMergeBase(target, source) {
var result = Object.assign({}, target);
for (var key in source) {
if (source.hasOwnProperty(key)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = Object.assign({}, target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
}
/**
* Get animation frame based on time
* @param {Object} anim - Animation definition
* @param {number} startTime - Animation start time
* @param {boolean} loop - Whether to loop
* @returns {Object} Frame info with index and complete flag
*/
function getAnimationFrame(anim, startTime, loop) {
var elapsed = performance.now() - startTime;
var totalDuration = anim.frameDuration * anim.frameCount;
if (!loop && elapsed >= totalDuration) {
return { frame: anim.frameCount - 1, complete: true };
}
var frameIndex = Math.floor((elapsed / anim.frameDuration) % anim.frameCount);
return { frame: frameIndex, complete: false };
}
// ─── Public API ──────────────────────────────────────
window.AvatarSystem = {
// ─── Creation ──────────────────────────────────────
/**
* Create a new avatar from customization choices
* @param {Object} customization - Full base customization object
* @returns {Object} Complete avatar object
*/
create: function(customization) {
var avatar = createDefault();
if (customization) {
avatar.base = deepMergeBase(avatar.base, customization);
}
return avatar;
},
/**
* Load user's saved avatar (from API or storage)
* @param {string} userId - User ID to load avatar for
* @returns {Promise<Object>} Avatar object
*/
loadUserAvatar: function(userId) {
var self = this;
return new Promise(function(resolve) {
// Check cache first
if (avatarCache.has(userId)) {
resolve(cloneAvatar(avatarCache.get(userId)));
return;
}
// In production, this would fetch from API
// For now, check localStorage as fallback
var storageKey = 'avatar_' + userId;
var stored = localStorage.getItem(storageKey);
if (stored) {
try {
var parsed = JSON.parse(stored);
if (validateAvatar(parsed)) {
avatarCache.set(userId, parsed);
resolve(cloneAvatar(parsed));
return;
}
} catch (e) {
console.warn('AvatarSystem: Failed to parse stored avatar', e);
}
}
// Return a guest avatar for unknown users
var guest = typeof AvatarData !== 'undefined' && AvatarData.getRandomGuestAvatar
? AvatarData.getRandomGuestAvatar()
: { base: createDefault().base };
var avatar = createDefault();
avatar.userId = userId;
avatar.base = Object.assign({}, avatar.base, guest.base);
resolve(avatar);
});
},
/**
* Save avatar to cache (and would persist to API)
* @param {Object} avatar - Avatar to save
* @returns {Object} Saved avatar
*/
saveAvatar: function(avatar) {
if (avatar.userId) {
avatarCache.set(avatar.userId, cloneAvatar(avatar));
// Also save to localStorage for persistence
var storageKey = 'avatar_' + avatar.userId;
try {
localStorage.setItem(storageKey, JSON.stringify(avatar));
} catch (e) {
console.warn('AvatarSystem: Failed to save avatar to localStorage', e);
}
}
return avatar;
},
/**
* Clear avatar from cache
* @param {string} userId - User ID to clear
*/
clearCache: function(userId) {
if (userId) {
avatarCache.delete(userId);
} else {
avatarCache.clear();
}
},
// ─── Rendering ─────────────────────────────────────
/**
* Render avatar in specified mode
* @param {Object} avatar - Avatar object
* @param {string} mode - 'HEAD_ONLY', 'HEAD_AND_SHOULDERS', 'UPPER_BODY', 'FULL_BODY'
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - Center X position
* @param {number} y - Center Y position
* @param {Object} options - { scale, animation, frame, time }
*/
render: function(avatar, mode, ctx, x, y, options) {
options = options || {};
var scale = options.scale || 1;
var time = options.time !== undefined ? options.time : performance.now() / 1000;
// Get animation transforms if animating
var transforms = {};
if (options.animation && typeof AvatarAnimations !== 'undefined') {
var anim = AvatarAnimations.getAnimation(mode, options.animation);
if (anim) {
var frame;
if (options.frame !== undefined) {
frame = options.frame;
} else {
frame = Math.floor((time * 1000 / anim.frameDuration) % anim.frameCount);
}
transforms = anim.frames[frame] || {};
}
}
// Render base avatar
if (typeof AvatarRenderer !== 'undefined') {
AvatarRenderer.render(ctx, avatar, mode, x, y, {
scale: scale,
transforms: transforms
});
} else {
console.warn('AvatarSystem: AvatarRenderer not loaded');
}
// Render accessories on top
if (typeof AccessoryRenderer !== 'undefined') {
AccessoryRenderer.render(ctx, avatar, mode, x, y, scale, time);
}
},
/**
* Render avatar to an offscreen canvas
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @param {Object} options - Render options
* @returns {HTMLCanvasElement} Canvas with rendered avatar
*/
renderToCanvas: function(avatar, mode, options) {
options = options || {};
var modeData = this.getModeSize(mode);
var scale = options.scale || 1;
var canvas = document.createElement('canvas');
canvas.width = modeData.width * scale;
canvas.height = modeData.height * scale;
var ctx = canvas.getContext('2d');
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
this.render(avatar, mode, ctx, centerX, centerY, options);
return canvas;
},
/**
* Render avatar to a data URL
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @param {Object} options - Render options
* @returns {string} Data URL of rendered avatar
*/
renderToDataURL: function(avatar, mode, options) {
var canvas = this.renderToCanvas(avatar, mode, options);
return canvas.toDataURL('image/png');
},
// ─── Animation ─────────────────────────────────────
/**
* Get current animation frame transforms
* @param {Object} avatar - Avatar object (unused but kept for API consistency)
* @param {string} animation - Animation ID
* @param {number} frame - Frame number
* @returns {Object} Transform data for the frame
*/
animate: function(avatar, animation, frame) {
if (typeof AvatarAnimations === 'undefined') {
return {};
}
var anim = AvatarAnimations.getAnimation('FULL_BODY', animation);
if (!anim) return {};
var f = frame % anim.frameCount;
return anim.frames[f] || {};
},
/**
* Start an animation for an avatar
* @param {string} avatarId - Avatar/user ID
* @param {string} animation - Animation ID
* @param {Object} options - { loop, onComplete, mode }
*/
startAnimation: function(avatarId, animation, options) {
options = options || {};
animationState.set(avatarId, {
animation: animation,
startTime: performance.now(),
loop: options.loop !== false,
mode: options.mode || 'FULL_BODY',
onComplete: options.onComplete || null
});
},
/**
* Stop animation for an avatar
* @param {string} avatarId - Avatar/user ID
*/
stopAnimation: function(avatarId) {
var state = animationState.get(avatarId);
if (state && state.onComplete) {
state.onComplete();
}
animationState.delete(avatarId);
},
/**
* Get current animation state for an avatar
* @param {string} avatarId - Avatar/user ID
* @returns {Object|null} Current animation state or null
*/
getAnimationState: function(avatarId) {
var state = animationState.get(avatarId);
if (!state) return null;
if (typeof AvatarAnimations === 'undefined') {
return { animation: state.animation, frame: 0, complete: false };
}
var anim = AvatarAnimations.getAnimation(state.mode, state.animation);
if (!anim) return null;
var frameInfo = getAnimationFrame(anim, state.startTime, state.loop);
// Handle completion
if (frameInfo.complete && !state.loop) {
if (state.onComplete) {
state.onComplete();
}
animationState.delete(avatarId);
}
return {
animation: state.animation,
frame: frameInfo.frame,
complete: frameInfo.complete,
transforms: anim.frames[frameInfo.frame] || {}
};
},
/**
* Check if avatar is currently animating
* @param {string} avatarId - Avatar/user ID
* @returns {boolean} True if animating
*/
isAnimating: function(avatarId) {
return animationState.has(avatarId);
},
// ─── Accessories ───────────────────────────────────
/**
* Equip an accessory
* @param {Object} avatar - Avatar object
* @param {string} slot - 'eyewear', 'headwear', 'effect'
* @param {string} itemId - Accessory ID or null to unequip
* @returns {boolean} True if successfully equipped
*/
equipAccessory: function(avatar, slot, itemId) {
if (!avatar.equipped) {
avatar.equipped = { eyewear: null, headwear: null, effect: null };
}
// Allow unequipping (null/undefined)
if (!itemId) {
avatar.equipped[slot] = null;
return true;
}
// Verify user has earned this item
var earnedKey = slot === 'effect' ? 'effects' : slot;
var earned = avatar.earned && avatar.earned[earnedKey];
if (!earned || !earned.includes(itemId)) {
console.warn('AvatarSystem: Cannot equip unearned item', itemId);
return false;
}
avatar.equipped[slot] = itemId;
return true;
},
/**
* Unequip an accessory from a slot
* @param {Object} avatar - Avatar object
* @param {string} slot - 'eyewear', 'headwear', 'effect'
* @returns {boolean} True if successfully unequipped
*/
unequipAccessory: function(avatar, slot) {
return this.equipAccessory(avatar, slot, null);
},
/**
* Unlock/award an accessory to a user
* @param {Object} avatar - Avatar object
* @param {string} itemId - Accessory ID to unlock
* @returns {boolean} True if newly unlocked, false if already owned
*/
unlockAccessory: function(avatar, itemId) {
if (typeof AvatarAccessories === 'undefined') {
console.warn('AvatarSystem: AvatarAccessories not loaded');
return false;
}
var item = AvatarAccessories.getById(itemId);
if (!item) {
console.warn('AvatarSystem: Unknown accessory', itemId);
return false;
}
var category = item.category === 'effect' ? 'effects' : item.category;
// Initialize earned structure if needed
if (!avatar.earned) {
avatar.earned = { eyewear: [], headwear: [], effects: [] };
}
if (!avatar.earned[category]) {
avatar.earned[category] = [];
}
// Check if already owned
if (avatar.earned[category].includes(itemId)) {
return false;
}
avatar.earned[category].push(itemId);
return true;
},
/**
* Get all accessories a user has earned
* @param {Object} avatar - Avatar object
* @returns {Object} Object with eyewear, headwear, effects arrays of accessory objects
*/
getEarnedAccessories: function(avatar) {
var result = { eyewear: [], headwear: [], effects: [] };
if (!avatar.earned) return result;
if (typeof AvatarAccessories === 'undefined') {
return result;
}
if (avatar.earned.eyewear) {
result.eyewear = avatar.earned.eyewear
.map(function(id) { return AvatarAccessories.getById(id); })
.filter(function(item) { return item !== null; });
}
if (avatar.earned.headwear) {
result.headwear = avatar.earned.headwear
.map(function(id) { return AvatarAccessories.getById(id); })
.filter(function(item) { return item !== null; });
}
if (avatar.earned.effects) {
result.effects = avatar.earned.effects
.map(function(id) { return AvatarAccessories.getById(id); })
.filter(function(item) { return item !== null; });
}
return result;
},
/**
* Get currently equipped accessories
* @param {Object} avatar - Avatar object
* @returns {Object} Object with equipped accessory objects
*/
getEquippedAccessories: function(avatar) {
var result = { eyewear: null, headwear: null, effect: null };
if (!avatar.equipped) return result;
if (typeof AvatarAccessories === 'undefined') {
return result;
}
if (avatar.equipped.eyewear) {
result.eyewear = AvatarAccessories.getById(avatar.equipped.eyewear);
}
if (avatar.equipped.headwear) {
result.headwear = AvatarAccessories.getById(avatar.equipped.headwear);
}
if (avatar.equipped.effect) {
result.effect = AvatarAccessories.getById(avatar.equipped.effect);
}
return result;
},
/**
* Check what accessories should unlock at a level
* @param {Object} avatar - Avatar object
* @param {number} newLevel - New level reached
* @returns {Array} Array of newly unlocked accessory objects
*/
checkLevelUnlocks: function(avatar, newLevel) {
if (typeof AvatarAccessories === 'undefined') {
return [];
}
var newItems = AvatarAccessories.getNewlyUnlockedAtLevel(newLevel);
var unlocked = [];
var self = this;
newItems.forEach(function(item) {
if (self.unlockAccessory(avatar, item.id)) {
unlocked.push(item);
}
});
return unlocked;
},
/**
* Get the full accessory catalog
* @returns {Object} Full accessory catalog
*/
getAccessoryCatalog: function() {
if (typeof AvatarAccessories === 'undefined') {
return { eyewear: [], headwear: [], effects: [] };
}
return AvatarAccessories.getCatalog();
},
/**
* Check if avatar has a specific accessory
* @param {Object} avatar - Avatar object
* @param {string} itemId - Accessory ID
* @returns {boolean} True if owned
*/
hasAccessory: function(avatar, itemId) {
if (!avatar.earned) return false;
for (var category in avatar.earned) {
if (avatar.earned[category] && avatar.earned[category].includes(itemId)) {
return true;
}
}
return false;
},
// ─── Export for Games ──────────────────────────────
/**
* Prepare avatar for use in a game
* Returns a simplified object optimized for game rendering
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @returns {Object} Game-ready avatar object
*/
exportForGame: function(avatar, mode) {
var self = this;
var modeData = this.getModeSize(mode);
var cloned = cloneAvatar(avatar);
var animations = {};
if (typeof AvatarAnimations !== 'undefined') {
animations = AvatarAnimations.getAnimationsForMode(mode);
}
return {
mode: mode,
width: modeData.width,
height: modeData.height,
avatar: cloned,
animations: animations,
/**
* Render the avatar
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - X position
* @param {number} y - Y position
* @param {Object} options - Render options
*/
render: function(ctx, x, y, options) {
self.render(cloned, mode, ctx, x, y, options);
},
/**
* Get animation transforms
* @param {string} animation - Animation ID
* @param {number} frame - Frame number
* @returns {Object} Transform data
*/
animate: function(animation, frame) {
return self.animate(cloned, animation, frame);
},
/**
* Get animation info
* @param {string} animationId - Animation ID
* @returns {Object|null} Animation info
*/
getAnimationInfo: function(animationId) {
if (typeof AvatarAnimations === 'undefined') return null;
return AvatarAnimations.getAnimation(mode, animationId);
}
};
},
/**
* Export avatar as minimal data for network transmission
* @param {Object} avatar - Avatar object
* @returns {Object} Minimal avatar data
*/
exportMinimal: function(avatar) {
return {
base: avatar.base,
equipped: avatar.equipped,
level: avatar.level
};
},
/**
* Import avatar from minimal data
* @param {Object} minimalData - Minimal avatar data
* @param {string} userId - User ID
* @returns {Object} Full avatar object
*/
importMinimal: function(minimalData, userId) {
var avatar = createDefault();
avatar.userId = userId;
if (minimalData.base) {
avatar.base = deepMergeBase(avatar.base, minimalData.base);
}
if (minimalData.equipped) {
avatar.equipped = Object.assign({}, avatar.equipped, minimalData.equipped);
}
if (minimalData.level !== undefined) {
avatar.level = minimalData.level;
}
return avatar;
},
// ─── Guest Avatars ─────────────────────────────────
/**
* Get a random guest avatar (for non-logged-in users)
* @returns {Object} Guest avatar object
*/
getGuestAvatar: function() {
var guest;
if (typeof AvatarData !== 'undefined' && AvatarData.getRandomGuestAvatar) {
guest = AvatarData.getRandomGuestAvatar();
} else {
guest = { base: createDefault().base };
}
var avatar = createDefault();
avatar.userId = 'guest_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
avatar.base = Object.assign({}, avatar.base, guest.base);
avatar.isGuest = true;
return avatar;
},
/**
* Check if avatar is a guest
* @param {Object} avatar - Avatar object
* @returns {boolean} True if guest avatar
*/
isGuest: function(avatar) {
return avatar.isGuest === true || (avatar.userId && avatar.userId.startsWith('guest_'));
},
/**
* Convert guest avatar to registered user avatar
* @param {Object} guestAvatar - Guest avatar
* @param {string} userId - New user ID
* @returns {Object} Converted avatar
*/
convertGuestToUser: function(guestAvatar, userId) {
var avatar = cloneAvatar(guestAvatar);
avatar.userId = userId;
delete avatar.isGuest;
return avatar;
},
// ─── Level & XP ────────────────────────────────────
/**
* Add XP to avatar and check for level ups
* @param {Object} avatar - Avatar object
* @param {number} xpAmount - XP to add
* @returns {Object} Result with newLevel, leveledUp, unlockedItems
*/
addXP: function(avatar, xpAmount) {
var oldLevel = avatar.level || 1;
avatar.xp = (avatar.xp || 0) + xpAmount;
// Calculate new level (100 XP per level, increasing by 50 each level)
var xpNeeded = 0;
var level = 0;
var remainingXP = avatar.xp;
while (remainingXP >= 0) {
level++;
var xpForLevel = 100 + (level - 1) * 50;
if (remainingXP < xpForLevel) break;
remainingXP -= xpForLevel;
}
avatar.level = level;
var result = {
newLevel: level,
leveledUp: level > oldLevel,
unlockedItems: []
};
// Check for unlocks at each new level
if (result.leveledUp) {
for (var l = oldLevel + 1; l <= level; l++) {
var unlocked = this.checkLevelUnlocks(avatar, l);
result.unlockedItems = result.unlockedItems.concat(unlocked);
}
}
return result;
},
/**
* Get XP needed for next level
* @param {Object} avatar - Avatar object
* @returns {Object} XP info
*/
getXPInfo: function(avatar) {
var level = avatar.level || 1;
var totalXP = avatar.xp || 0;
// Calculate XP used for previous levels
var xpUsed = 0;
for (var l = 1; l < level; l++) {
xpUsed += 100 + (l - 1) * 50;
}
var currentLevelXP = totalXP - xpUsed;
var xpForCurrentLevel = 100 + (level - 1) * 50;
return {
level: level,
totalXP: totalXP,
currentLevelXP: currentLevelXP,
xpNeededForLevel: xpForCurrentLevel,
progress: currentLevelXP / xpForCurrentLevel
};
},
// ─── Utilities ─────────────────────────────────────
/**
* Create a sprite sheet for an avatar
* Useful for game performance
* @param {Object} avatar - Avatar object
* @param {string} mode - Rendering mode
* @param {Array} animations - Animation IDs (optional, defaults to all)
* @returns {Object} Sprite sheet data
*/
createSpriteSheet: function(avatar, mode, animations) {
var modeData = this.getModeSize(mode);
var self = this;
// Get animation list
if (!animations && typeof AvatarAnimations !== 'undefined') {
var allAnims = AvatarAnimations.getAnimationsForMode(mode);
animations = Object.keys(allAnims);
}
animations = animations || [];
// Calculate sprite sheet dimensions
var maxFrames = 0;
var animData = [];
animations.forEach(function(animId) {
if (typeof AvatarAnimations !== 'undefined') {
var anim = AvatarAnimations.getAnimation(mode, animId);
if (anim) {
maxFrames = Math.max(maxFrames, anim.frameCount);
animData.push({ id: animId, frames: anim.frameCount, duration: anim.frameDuration });
}
}
});
// Handle case with no animations
if (animData.length === 0) {
animData.push({ id: 'idle', frames: 1, duration: 100 });
maxFrames = 1;
}
// Create canvas
var canvas = document.createElement('canvas');
canvas.width = modeData.width * maxFrames;
canvas.height = modeData.height * animData.length;
var ctx = canvas.getContext('2d');
// Render each animation frame
animData.forEach(function(anim, row) {
for (var f = 0; f < anim.frames; f++) {
var x = modeData.width * f + modeData.width / 2;
var y = modeData.height * row + modeData.height / 2;
self.render(avatar, mode, ctx, x, y, { animation: anim.id, frame: f });
}
});
return {
canvas: canvas,
image: canvas,
animations: animData,
frameWidth: modeData.width,
frameHeight: modeData.height,
/**
* Get frame coordinates for an animation frame
* @param {string} animId - Animation ID
* @param {number} frame - Frame number
* @returns {Object} Source rectangle {x, y, width, height}
*/
getFrameRect: function(animId, frame) {
var row = 0;
for (var i = 0; i < animData.length; i++) {
if (animData[i].id === animId) {
row = i;
break;
}
}
var f = frame % (animData[row] ? animData[row].frames : 1);
return {
x: f * modeData.width,
y: row * modeData.height,
width: modeData.width,
height: modeData.height
};
}
};
},
/**
* Get rendering mode dimensions
* @param {string} mode - Rendering mode
* @returns {Object} Mode dimensions {width, height}
*/
getModeSize: function(mode) {
if (typeof AvatarAnimations !== 'undefined' && AvatarAnimations.getMode) {
return AvatarAnimations.getMode(mode);
}
// Fallback defaults
var defaults = {
'HEAD_ONLY': { width: 64, height: 64 },
'HEAD_AND_SHOULDERS': { width: 96, height: 96 },
'UPPER_BODY': { width: 128, height: 128 },
'FULL_BODY': { width: 128, height: 192 }
};
return defaults[mode] || defaults['HEAD_ONLY'];
},
/**
* Validate avatar data structure
* @param {Object} avatar - Avatar to validate
* @returns {boolean} True if valid
*/
validate: validateAvatar,
/**
* Create default avatar
* @returns {Object} Default avatar object
*/
createDefault: createDefault,
/**
* Clone an avatar object
* @param {Object} avatar - Avatar to clone
* @returns {Object} Cloned avatar
*/
clone: cloneAvatar,
/**
* Update avatar base properties
* @param {Object} avatar - Avatar to update
* @param {Object} updates - Properties to update
* @returns {Object} Updated avatar
*/
updateBase: function(avatar, updates) {
avatar.base = deepMergeBase(avatar.base, updates);
return avatar;
},
/**
* Get available customization options
* @returns {Object} Available options from AvatarData
*/
getCustomizationOptions: function() {
if (typeof AvatarData === 'undefined') {
return {};
}
return {
faceShapes: AvatarData.FACE_SHAPES || [],
skinTones: AvatarData.SKIN_TONES || [],
eyeShapes: AvatarData.EYE_SHAPES || [],
eyeColors: AvatarData.EYE_COLORS || [],
eyebrowShapes: AvatarData.EYEBROW_SHAPES || [],
noseShapes: AvatarData.NOSE_SHAPES || [],
mouthShapes: AvatarData.MOUTH_SHAPES || [],
earShapes: AvatarData.EAR_SHAPES || [],
hairStyles: AvatarData.HAIR_STYLES || [],
hairColors: AvatarData.HAIR_COLORS || [],
facialHair: AvatarData.FACIAL_HAIR || [],
bodyTypes: AvatarData.BODY_TYPES || [],
clothingStyles: AvatarData.CLOTHING_STYLES || []
};
},
/**
* Compare two avatars for equality
* @param {Object} a - First avatar
* @param {Object} b - Second avatar
* @returns {boolean} True if equivalent
*/
equals: function(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
},
/**
* Get version info
* @returns {Object} Version information
*/
getVersion: function() {
return {
version: '1.0.0',
dependencies: ['AvatarData', 'AvatarAnimations', 'AvatarRenderer', 'AvatarAccessories', 'AccessoryRenderer']
};
}
};
})();
@@ -0,0 +1,262 @@
(function() {
'use strict';
// =========================================================================
// AGGREGATE ALL CONTEXTS
// =========================================================================
var ALL_CONTEXTS = [];
// Add vehicle contexts
if (window.VehicleContexts) {
VehicleContexts.getAll().forEach(function(ctx) {
ALL_CONTEXTS.push(ctx);
});
}
// Add costume contexts
if (window.CostumeContexts) {
CostumeContexts.getAll().forEach(function(ctx) {
ALL_CONTEXTS.push(ctx);
});
}
// Add pure contexts
if (window.PureContexts) {
PureContexts.getAll().forEach(function(ctx) {
ALL_CONTEXTS.push(ctx);
});
}
// Create lookup map
var CONTEXT_MAP = {};
ALL_CONTEXTS.forEach(function(ctx) {
CONTEXT_MAP[ctx.id] = ctx;
});
// =========================================================================
// CATEGORY METADATA
// =========================================================================
var CATEGORIES = {
vehicle: {
id: 'vehicle',
name: 'Vehicles',
description: 'Place your avatar in vehicles and cockpits',
icon: '🚗',
count: 0 // calculated
},
costume: {
id: 'costume',
name: 'Costumes',
description: 'Dress your avatar in themed outfits',
icon: '👔',
count: 0
},
pure: {
id: 'pure',
name: 'Standard',
description: 'Your avatar with minimal modifications',
icon: '🏃',
count: 0
}
};
// Count per category
ALL_CONTEXTS.forEach(function(ctx) {
if (CATEGORIES[ctx.category]) {
CATEGORIES[ctx.category].count++;
}
});
// =========================================================================
// GAME TYPE MAPPINGS
// =========================================================================
var GAME_TYPE_CONTEXTS = {
platformer: ['platformer-standard', 'adventure-hero', 'knight-armor', 'ninja-outfit', 'superhero-cape'],
racing: ['race-car', 'motorcycle', 'hoverboard', 'airplane'],
shooter: ['spaceship-cockpit', 'tank', 'mech-suit', 'space-suit'],
flying: ['flappy-style', 'airplane', 'jetpack', 'dragon-rider'],
arcade: ['pacman-style', 'flappy-style', 'spaceship-cockpit', 'runner'],
rpg: ['knight-armor', 'wizard-robes', 'ninja-outfit', 'pirate-outfit', 'adventure-hero'],
sports: ['sports-player', 'athlete-uniform', 'runner'],
puzzle: ['scientist-labcoat', 'wizard-robes', 'platformer-standard'],
adventure: ['adventure-hero', 'pirate-outfit', 'cowboy-gear', 'dragon-rider'],
action: ['superhero-cape', 'ninja-outfit', 'mech-suit', 'jetpack'],
underwater: ['submarine', 'space-suit'],
space: ['spaceship-cockpit', 'space-suit', 'mech-suit'],
cooking: ['chef-outfit'],
simulation: ['airplane', 'submarine', 'race-car', 'tank']
};
// =========================================================================
// MODE GROUPINGS
// =========================================================================
var MODE_CONTEXTS = {
HEAD_ONLY: [],
HEAD_AND_SHOULDERS: [],
UPPER_BODY: [],
FULL_BODY: []
};
ALL_CONTEXTS.forEach(function(ctx) {
if (MODE_CONTEXTS[ctx.avatarMode]) {
MODE_CONTEXTS[ctx.avatarMode].push(ctx.id);
}
});
// =========================================================================
// CATALOG STATISTICS
// =========================================================================
var STATS = {
totalContexts: ALL_CONTEXTS.length,
vehicleCount: CATEGORIES.vehicle.count,
costumeCount: CATEGORIES.costume.count,
pureCount: CATEGORIES.pure.count,
version: '1.0.0',
lastUpdated: '2026-02-05'
};
// =========================================================================
// PUBLIC API
// =========================================================================
window.ContextCatalog = {
// Core getters
getAll: function() {
return ALL_CONTEXTS.slice();
},
getById: function(id) {
return CONTEXT_MAP[id] || null;
},
// Category queries
getByCategory: function(category) {
return ALL_CONTEXTS.filter(function(ctx) {
return ctx.category === category;
});
},
getCategories: function() {
return Object.values(CATEGORIES);
},
getCategoryInfo: function(categoryId) {
return CATEGORIES[categoryId] || null;
},
// Mode queries
getByMode: function(mode) {
var ids = MODE_CONTEXTS[mode] || [];
return ids.map(function(id) {
return CONTEXT_MAP[id];
}).filter(Boolean);
},
getModes: function() {
return Object.keys(MODE_CONTEXTS);
},
// Game type recommendations
getRecommendedFor: function(gameType) {
var ids = GAME_TYPE_CONTEXTS[gameType] || [];
return ids.map(function(id) {
return CONTEXT_MAP[id];
}).filter(Boolean);
},
getGameTypes: function() {
return Object.keys(GAME_TYPE_CONTEXTS);
},
// Search/filter
search: function(query) {
query = query.toLowerCase();
return ALL_CONTEXTS.filter(function(ctx) {
return ctx.id.toLowerCase().includes(query) ||
ctx.name.toLowerCase().includes(query) ||
ctx.description.toLowerCase().includes(query);
});
},
filter: function(filters) {
return ALL_CONTEXTS.filter(function(ctx) {
if (filters.category && ctx.category !== filters.category) return false;
if (filters.mode && ctx.avatarMode !== filters.mode) return false;
if (filters.gameType) {
var recommended = GAME_TYPE_CONTEXTS[filters.gameType] || [];
if (recommended.indexOf(ctx.id) === -1) return false;
}
return true;
});
},
// For UI display
getCatalogForUI: function() {
return {
categories: Object.values(CATEGORIES).map(function(cat) {
return {
id: cat.id,
name: cat.name,
description: cat.description,
icon: cat.icon,
contexts: ALL_CONTEXTS.filter(function(ctx) {
return ctx.category === cat.id;
}).map(function(ctx) {
return {
id: ctx.id,
name: ctx.name,
description: ctx.description,
mode: ctx.avatarMode,
recommendedFor: ctx.recommendedFor
};
})
};
})
};
},
// Random selection
getRandom: function(filters) {
var pool = filters ? this.filter(filters) : ALL_CONTEXTS;
if (pool.length === 0) return null;
return pool[Math.floor(Math.random() * pool.length)];
},
getRandomForGameType: function(gameType) {
var recommended = this.getRecommendedFor(gameType);
if (recommended.length === 0) return null;
return recommended[Math.floor(Math.random() * recommended.length)];
},
// Statistics
getStats: function() {
return Object.assign({}, STATS);
},
// Validation
validate: function() {
var issues = [];
ALL_CONTEXTS.forEach(function(ctx) {
if (!ctx.id) issues.push('Context missing id');
if (!ctx.name) issues.push(ctx.id + ': missing name');
if (!ctx.avatarMode) issues.push(ctx.id + ': missing avatarMode');
if (!ctx.category) issues.push(ctx.id + ': missing category');
if (!ctx.animations || ctx.animations.length === 0) {
issues.push(ctx.id + ': missing animations');
}
});
return {
valid: issues.length === 0,
issues: issues
};
}
};
})();
@@ -0,0 +1,959 @@
/**
* ContextRenderer.js - Main renderer for applying contexts to avatars
*
* Handles the complete rendering pipeline for avatars with contexts,
* including vehicles, costumes, and pure animation contexts.
*
* Dependencies (must be loaded before this file):
* - window.AvatarSystem
* - window.AvatarRenderer
* - window.AvatarAnimations
* - window.AccessoryRenderer
* - window.VehicleContexts
* - window.CostumeContexts
* - window.PureContexts
*/
(function() {
'use strict';
// ============================================================
// CONTEXT APPLICATION
// ============================================================
/**
* Apply a context to an avatar, creating a renderable object
* @param {Object} avatar - The avatar object
* @param {string} contextId - The context ID to apply
* @param {Object} options - Optional configuration
* @returns {Object} Avatar with context object
*/
function apply(avatar, contextId, options) {
var context = getContextById(contextId);
if (!context) {
console.error('ContextRenderer: Unknown context ID:', contextId);
return null;
}
// Validate avatar has required mode or can be rendered in it
var mode = context.avatarMode;
if (!mode) {
console.warn('ContextRenderer: Context missing avatarMode:', contextId);
mode = 'profile'; // Default fallback
}
return {
avatar: avatar,
context: context,
mode: mode,
state: {
animation: context.defaultAnimation || 'idle',
frame: 0,
time: 0,
animationStartTime: 0,
animationOptions: {},
effects: [],
props: {},
custom: {}
},
options: options || {}
};
}
// ============================================================
// MAIN RENDER FUNCTION
// ============================================================
/**
* Render complete scene with avatar and context
* @param {Object} avatarWithContext - The avatar+context object from apply()
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - Center X position
* @param {number} y - Center Y position
* @param {Object} options - Render options (scale, time, etc)
*/
function render(avatarWithContext, ctx, x, y, options) {
if (!avatarWithContext || !avatarWithContext.context) {
console.error('ContextRenderer.render: Invalid avatarWithContext');
return;
}
options = options || {};
var scale = options.scale || 1;
var time = options.time || 0;
var awc = avatarWithContext;
var context = awc.context;
var avatar = awc.avatar;
var mode = awc.mode;
// Get context dimensions
var contextWidth = context.width || 64;
var contextHeight = context.height || 64;
var scaledWidth = contextWidth * scale;
var scaledHeight = contextHeight * scale;
// Get animation transforms
var transforms = {};
if (awc.state.animation && window.AvatarAnimations) {
var anim = window.AvatarAnimations.getAnimation(mode, awc.state.animation);
if (anim) {
var progress = anim.frameCount > 0 ? awc.state.frame / anim.frameCount : 0;
transforms = window.AvatarAnimations.interpolateFrame(anim, progress);
}
}
// Calculate avatar position within context
var avatarPosX = context.avatarPosition ? context.avatarPosition.x : 0.5;
var avatarPosY = context.avatarPosition ? context.avatarPosition.y : 0.5;
var avatarX = x + (avatarPosX - 0.5) * scaledWidth;
var avatarY = y + (avatarPosY - 0.5) * scaledHeight;
var avatarScale = scale * (context.avatarScale || 1);
// Route to appropriate render pipeline based on category
var category = context.category || 'pure';
if (category === 'vehicle') {
renderVehicleContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, scaledWidth, scaledHeight, transforms, time);
} else if (category === 'costume') {
renderCostumeContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, scaledWidth, scaledHeight, transforms, time);
} else {
// Pure context (default)
renderPureContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, scaledWidth, scaledHeight, transforms, time);
}
}
// ============================================================
// VEHICLE CONTEXT RENDERING
// ============================================================
function renderVehicleContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, width, height, transforms, time) {
// 1. Context background (vehicle body behind avatar)
if (context.renderBackground && typeof context.renderBackground === 'function') {
ctx.save();
context.renderBackground(ctx, width, height, time, awc.state);
ctx.restore();
}
// 2. Render avatar
if (window.AvatarRenderer && window.AvatarRenderer.render) {
window.AvatarRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, {
scale: avatarScale,
transforms: transforms
});
}
// 3. Render accessories respecting visibility rules
renderAccessoriesForContext(ctx, avatar, context, avatarX, avatarY, avatarScale, time);
// 4. Context foreground (steering wheel, windshield, etc)
if (context.renderForeground && typeof context.renderForeground === 'function') {
ctx.save();
context.renderForeground(ctx, width, height, time, awc.state);
ctx.restore();
}
// 5. Context effects (exhaust, speed lines, etc)
renderContextEffects(ctx, awc, x, y, scale, time);
}
// ============================================================
// COSTUME CONTEXT RENDERING
// ============================================================
function renderCostumeContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, width, height, transforms, time) {
// 1. Costume under-layer (body armor, suit body, etc)
if (context.renderCostumeUnder && typeof context.renderCostumeUnder === 'function') {
ctx.save();
context.renderCostumeUnder(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time);
ctx.restore();
}
// 2. Avatar HEAD ONLY (body is covered by costume)
renderAvatarHeadOnly(ctx, avatar, avatarX, avatarY, avatarScale, transforms);
// 3. Costume over-layer (helmet, held items, cape)
if (context.renderCostumeOver && typeof context.renderCostumeOver === 'function') {
ctx.save();
context.renderCostumeOver(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time);
ctx.restore();
}
// 4. EARNED ACCESSORIES ON TOP (crown shows above helmet, etc)
renderEarnedAccessoriesOnTop(ctx, avatar, context, avatarX, avatarY, avatarScale, time);
// 5. Effects (auras are always visible)
renderEffectsForContext(ctx, avatar, context, avatarX, avatarY, avatarScale, time);
// 6. Context-specific effects
renderContextEffects(ctx, awc, x, y, scale, time);
}
// ============================================================
// PURE CONTEXT RENDERING
// ============================================================
function renderPureContext(ctx, awc, avatar, context, x, y, avatarX, avatarY,
scale, avatarScale, width, height, transforms, time) {
// 1. Behind effects (shadows, dust clouds, etc)
if (context.renderEffectsBehind && typeof context.renderEffectsBehind === 'function') {
ctx.save();
context.renderEffectsBehind(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time, awc.state);
ctx.restore();
}
// 2. Full avatar with all accessories
if (window.AvatarRenderer && window.AvatarRenderer.render) {
window.AvatarRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, {
scale: avatarScale,
transforms: transforms
});
}
if (window.AccessoryRenderer && window.AccessoryRenderer.render) {
window.AccessoryRenderer.render(ctx, avatar, awc.mode, avatarX, avatarY, avatarScale, time);
}
// 3. Over effects (speed lines, stars, sparkles)
if (context.renderEffectsOver && typeof context.renderEffectsOver === 'function') {
ctx.save();
context.renderEffectsOver(ctx, avatar, avatarX, avatarY, avatarScale, transforms, time, awc.state);
ctx.restore();
}
// 4. Active state effects
renderContextEffects(ctx, awc, x, y, scale, time);
}
// ============================================================
// ANIMATION CONTROL
// ============================================================
/**
* Start an animation on an avatar with context
* @param {Object} avatarWithContext - The avatar+context object
* @param {string} animationId - Animation ID to play
* @param {Object} options - Animation options
* @returns {boolean} True if animation started successfully
*/
function animate(avatarWithContext, animationId, options) {
if (!avatarWithContext || !avatarWithContext.context) {
console.error('ContextRenderer.animate: Invalid avatarWithContext');
return false;
}
var awc = avatarWithContext;
var context = awc.context;
// Validate animation is available for this context
if (context.animations && Array.isArray(context.animations)) {
if (context.animations.indexOf(animationId) === -1) {
console.warn('ContextRenderer: Animation not available for context:', animationId, 'in', context.id);
return false;
}
}
// Check if animation exists in the animation system
if (window.AvatarAnimations) {
var anim = window.AvatarAnimations.getAnimation(awc.mode, animationId);
if (!anim) {
console.warn('ContextRenderer: Animation not found:', animationId, 'for mode', awc.mode);
return false;
}
}
// Set animation state
awc.state.animation = animationId;
awc.state.frame = 0;
awc.state.time = 0;
awc.state.animationStartTime = performance.now();
awc.state.animationOptions = options || {};
// Trigger context-specific effects for this animation
if (context.contextEffects && context.contextEffects[animationId]) {
triggerEffect(awc, context.contextEffects[animationId]);
}
return true;
}
/**
* Update animation state based on elapsed time
* @param {Object} avatarWithContext - The avatar+context object
* @param {number} deltaTime - Time elapsed in seconds
*/
function updateAnimation(avatarWithContext, deltaTime) {
if (!avatarWithContext) return;
var awc = avatarWithContext;
if (!awc.state.animation) return;
// Get animation definition
var anim = null;
if (window.AvatarAnimations) {
anim = window.AvatarAnimations.getAnimation(awc.mode, awc.state.animation);
}
if (!anim) return;
// Update time
awc.state.time += deltaTime;
// Calculate frame based on time
var frameDuration = (anim.frameDuration || 100) / 1000; // Convert ms to seconds
var totalFrames = anim.frameCount || 1;
if (anim.loop) {
// Looping animation
awc.state.frame = (awc.state.time / frameDuration) % totalFrames;
} else {
// One-shot animation
awc.state.frame = Math.min(awc.state.time / frameDuration, totalFrames - 1);
// Check if animation completed
if (awc.state.frame >= totalFrames - 1) {
// Trigger completion callback if set
if (awc.state.animationOptions && awc.state.animationOptions.onComplete) {
awc.state.animationOptions.onComplete();
}
}
}
// Update active effects
updateEffects(awc, deltaTime);
}
// ============================================================
// EFFECT SYSTEM
// ============================================================
/**
* Trigger a visual effect
* @param {Object} avatarWithContext - The avatar+context object
* @param {Object} effectDef - Effect definition
*/
function triggerEffect(avatarWithContext, effectDef) {
if (!avatarWithContext || !effectDef) return;
var awc = avatarWithContext;
// Handle effect definition as string (lookup) or object
var effect;
if (typeof effectDef === 'string') {
effect = {
type: effectDef,
color: '#ffffff',
startTime: performance.now(),
duration: 0.5,
intensity: 1,
data: {}
};
} else {
effect = {
type: effectDef.type || 'flash',
color: effectDef.color || '#ffffff',
startTime: performance.now(),
duration: effectDef.duration || 0.5,
intensity: effectDef.intensity || 1,
data: effectDef.data || {}
};
}
awc.state.effects.push(effect);
}
/**
* Update all active effects
* @param {Object} avatarWithContext - The avatar+context object
* @param {number} deltaTime - Time elapsed in seconds
*/
function updateEffects(avatarWithContext, deltaTime) {
if (!avatarWithContext) return;
var awc = avatarWithContext;
var now = performance.now();
// Filter out expired effects and update active ones
awc.state.effects = awc.state.effects.filter(function(effect) {
var elapsed = (now - effect.startTime) / 1000;
if (elapsed >= effect.duration) {
return false; // Remove expired effect
}
// Update effect progress
effect.progress = elapsed / effect.duration;
return true;
});
}
/**
* Clear all active effects
* @param {Object} avatarWithContext - The avatar+context object
*/
function clearEffects(avatarWithContext) {
if (!avatarWithContext) return;
avatarWithContext.state.effects = [];
}
/**
* Render context effects
*/
function renderContextEffects(ctx, awc, x, y, scale, time) {
if (!awc.state.effects || awc.state.effects.length === 0) return;
var now = performance.now();
awc.state.effects.forEach(function(effect) {
var elapsed = (now - effect.startTime) / 1000;
var progress = Math.min(elapsed / effect.duration, 1);
ctx.save();
switch (effect.type) {
case 'flash':
renderFlashEffect(ctx, x, y, scale, effect, progress);
break;
case 'burst':
renderBurstEffect(ctx, x, y, scale, effect, progress);
break;
case 'trail':
renderTrailEffect(ctx, x, y, scale, effect, progress);
break;
case 'glow':
renderGlowEffect(ctx, x, y, scale, effect, progress);
break;
case 'particles':
renderParticleEffect(ctx, x, y, scale, effect, progress, time);
break;
case 'shake':
// Shake is handled via transforms, not direct rendering
break;
case 'speedLines':
renderSpeedLinesEffect(ctx, x, y, scale, effect, progress);
break;
default:
// Unknown effect type - custom effects can be rendered by context
break;
}
ctx.restore();
});
}
// Effect renderers
function renderFlashEffect(ctx, x, y, scale, effect, progress) {
var alpha = (1 - progress) * effect.intensity;
var radius = 30 * scale * (1 + progress);
ctx.globalAlpha = alpha;
ctx.fillStyle = effect.color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
function renderBurstEffect(ctx, x, y, scale, effect, progress) {
var alpha = (1 - progress) * effect.intensity;
var rayCount = effect.data.rayCount || 8;
var maxLength = (effect.data.maxLength || 40) * scale;
var length = maxLength * progress;
ctx.globalAlpha = alpha;
ctx.strokeStyle = effect.color;
ctx.lineWidth = 2 * scale;
for (var i = 0; i < rayCount; i++) {
var angle = (i / rayCount) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(
x + Math.cos(angle) * length,
y + Math.sin(angle) * length
);
ctx.stroke();
}
}
function renderTrailEffect(ctx, x, y, scale, effect, progress) {
var alpha = (1 - progress) * effect.intensity * 0.5;
var trailLength = (effect.data.length || 20) * scale;
var direction = effect.data.direction || -1; // -1 = trailing left
var gradient = ctx.createLinearGradient(
x, y,
x + trailLength * direction, y
);
gradient.addColorStop(0, effect.color);
gradient.addColorStop(1, 'transparent');
ctx.globalAlpha = alpha;
ctx.fillStyle = gradient;
ctx.fillRect(x, y - 10 * scale, trailLength * direction, 20 * scale);
}
function renderGlowEffect(ctx, x, y, scale, effect, progress) {
var alpha = effect.intensity * (0.5 + 0.5 * Math.sin(progress * Math.PI * 2));
var radius = (effect.data.radius || 25) * scale;
var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, effect.color);
gradient.addColorStop(1, 'transparent');
ctx.globalAlpha = alpha;
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
function renderParticleEffect(ctx, x, y, scale, effect, progress, time) {
var particleCount = effect.data.count || 10;
var alpha = (1 - progress) * effect.intensity;
var spread = (effect.data.spread || 30) * scale;
ctx.globalAlpha = alpha;
ctx.fillStyle = effect.color;
// Use seeded random based on effect start time for consistency
var seed = effect.startTime;
for (var i = 0; i < particleCount; i++) {
var pseudoRandom = Math.sin(seed + i * 12.9898) * 43758.5453;
pseudoRandom = pseudoRandom - Math.floor(pseudoRandom);
var angle = pseudoRandom * Math.PI * 2;
var distance = spread * progress * (0.5 + pseudoRandom * 0.5);
var size = 2 * scale * (1 - progress);
ctx.beginPath();
ctx.arc(
x + Math.cos(angle) * distance,
y + Math.sin(angle) * distance - progress * 20 * scale, // Rise up
size,
0, Math.PI * 2
);
ctx.fill();
}
}
function renderSpeedLinesEffect(ctx, x, y, scale, effect, progress) {
var alpha = effect.intensity * (1 - progress * 0.5);
var lineCount = effect.data.lineCount || 5;
var lineLength = (effect.data.lineLength || 30) * scale;
ctx.globalAlpha = alpha;
ctx.strokeStyle = effect.color;
ctx.lineWidth = 1 * scale;
for (var i = 0; i < lineCount; i++) {
var yOffset = (i - lineCount / 2) * 8 * scale;
var xStart = x - 20 * scale - lineLength * progress;
ctx.beginPath();
ctx.moveTo(xStart, y + yOffset);
ctx.lineTo(xStart + lineLength, y + yOffset);
ctx.stroke();
}
}
// ============================================================
// HELPER RENDERING FUNCTIONS
// ============================================================
/**
* Render only the avatar's head (for costumes that cover body)
*/
function renderAvatarHeadOnly(ctx, avatar, x, y, scale, transforms) {
if (window.AvatarRenderer && window.AvatarRenderer.renderHead) {
window.AvatarRenderer.renderHead(ctx, avatar, x, y, {
scale: scale,
transforms: transforms
});
} else if (window.AvatarRenderer && window.AvatarRenderer.render) {
// Fallback: render full avatar if renderHead not available
// This is a compromise - costume should hide body anyway
ctx.save();
// Clip to head region (approximate)
var headY = y - 12 * scale; // Head is above center
var headRadius = 10 * scale;
ctx.beginPath();
ctx.arc(x, headY, headRadius * 1.5, 0, Math.PI * 2);
ctx.clip();
window.AvatarRenderer.render(ctx, avatar, 'profile', x, y, {
scale: scale,
transforms: transforms
});
ctx.restore();
}
}
/**
* Render accessories respecting context visibility rules
*/
function renderAccessoriesForContext(ctx, avatar, context, x, y, scale, time) {
if (!window.AccessoryRenderer) return;
var vis = context.accessoryVisibility || {};
// Default visibility if not specified
var showHeadwear = vis.headwear !== false;
var showHandheld = vis.handheld !== false;
var showEffects = vis.effects !== false;
var showFace = vis.face !== false;
// Use AccessoryRenderer with visibility filter
if (window.AccessoryRenderer.renderFiltered) {
window.AccessoryRenderer.renderFiltered(ctx, avatar, context.avatarMode || 'profile', x, y, scale, time, {
headwear: showHeadwear,
handheld: showHandheld,
effects: showEffects,
face: showFace
});
} else if (window.AccessoryRenderer.render) {
// Fallback to standard render if filtered not available
// Context-specific visibility would need to be handled differently
window.AccessoryRenderer.render(ctx, avatar, context.avatarMode || 'profile', x, y, scale, time);
}
}
/**
* Render EARNED accessories on top of everything (crowns above helmets, etc)
*/
function renderEarnedAccessoriesOnTop(ctx, avatar, context, x, y, scale, time) {
if (!window.AccessoryRenderer) return;
if (!avatar.accessories) return;
var vis = context.accessoryVisibility || {};
var costumeOffset = context.costumeHeadOffset || { x: 0, y: -5 }; // Crown sits higher on helmet
// Render headwear if visibility allows and avatar has it
if (vis.headwear === true && avatar.accessories.headwear) {
var headwearY = y + costumeOffset.y * scale;
var headwearX = x + costumeOffset.x * scale;
if (window.AccessoryRenderer.renderSingle) {
window.AccessoryRenderer.renderSingle(ctx, avatar, 'headwear', headwearX, headwearY, scale, time);
}
}
// Render effect accessories (auras, trails) if allowed
if (vis.effects === true && avatar.accessories.effect) {
if (window.AccessoryRenderer.renderSingle) {
window.AccessoryRenderer.renderSingle(ctx, avatar, 'effect', x, y, scale, time);
}
}
}
/**
* Render effect accessories (auras, etc) for costume context
*/
function renderEffectsForContext(ctx, avatar, context, x, y, scale, time) {
if (!window.AccessoryRenderer) return;
if (!avatar.accessories || !avatar.accessories.effect) return;
var vis = context.accessoryVisibility || {};
// Effects (auras) are almost always visible
if (vis.effects !== false) {
if (window.AccessoryRenderer.renderSingle) {
window.AccessoryRenderer.renderSingle(ctx, avatar, 'effect', x, y, scale, time);
}
}
}
// ============================================================
// CONTEXT LOOKUP
// ============================================================
/**
* Get context definition by ID
*/
function getContextById(contextId) {
// Check ContextCatalog first
if (window.ContextCatalog && window.ContextCatalog.getById) {
var context = window.ContextCatalog.getById(contextId);
if (context) return context;
}
// Fallback to individual context modules
if (window.VehicleContexts && window.VehicleContexts[contextId]) {
return window.VehicleContexts[contextId];
}
if (window.CostumeContexts && window.CostumeContexts[contextId]) {
return window.CostumeContexts[contextId];
}
if (window.PureContexts && window.PureContexts[contextId]) {
return window.PureContexts[contextId];
}
return null;
}
/**
* Get required avatar mode for a context
*/
function getRequiredMode(contextId) {
var context = getContextById(contextId);
return context ? context.avatarMode : null;
}
/**
* Get available animations for a context
*/
function getAvailableAnimations(contextId) {
var context = getContextById(contextId);
return context && context.animations ? context.animations.slice() : [];
}
// ============================================================
// STATE MANAGEMENT
// ============================================================
/**
* Set custom state value
*/
function setState(avatarWithContext, key, value) {
if (!avatarWithContext || !avatarWithContext.state) return;
avatarWithContext.state.custom[key] = value;
}
/**
* Get custom state value
*/
function getState(avatarWithContext, key) {
if (!avatarWithContext || !avatarWithContext.state) return undefined;
return avatarWithContext.state.custom[key];
}
/**
* Set prop state (for interactive elements)
*/
function setProp(avatarWithContext, propId, state) {
if (!avatarWithContext || !avatarWithContext.state) return;
avatarWithContext.state.props[propId] = state;
}
/**
* Get prop state
*/
function getProp(avatarWithContext, propId) {
if (!avatarWithContext || !avatarWithContext.state) return undefined;
return avatarWithContext.state.props[propId];
}
// ============================================================
// EXPORT UTILITIES
// ============================================================
/**
* Create a sprite sheet from avatar with context
* @param {Object} avatarWithContext - The avatar+context object
* @param {Array} animations - Array of animation IDs to include
* @param {Object} options - Sprite sheet options
* @returns {Object} Sprite sheet data { canvas, frames, meta }
*/
function createSpriteSheet(avatarWithContext, animations, options) {
if (!avatarWithContext) return null;
options = options || {};
var frameWidth = options.frameWidth || 64;
var frameHeight = options.frameHeight || 64;
var scale = options.scale || 1;
var awc = avatarWithContext;
var allFrames = [];
// Collect all frames for each animation
animations.forEach(function(animId) {
var anim = null;
if (window.AvatarAnimations) {
anim = window.AvatarAnimations.getAnimation(awc.mode, animId);
}
if (!anim) return;
for (var i = 0; i < anim.frameCount; i++) {
allFrames.push({
animation: animId,
frame: i,
frameCount: anim.frameCount
});
}
});
if (allFrames.length === 0) return null;
// Calculate sprite sheet dimensions
var cols = Math.ceil(Math.sqrt(allFrames.length));
var rows = Math.ceil(allFrames.length / cols);
var sheetWidth = cols * frameWidth;
var sheetHeight = rows * frameHeight;
// Create canvas
var canvas = document.createElement('canvas');
canvas.width = sheetWidth;
canvas.height = sheetHeight;
var ctx = canvas.getContext('2d');
// Render each frame
var frames = [];
allFrames.forEach(function(frameData, index) {
var col = index % cols;
var row = Math.floor(index / cols);
var x = col * frameWidth + frameWidth / 2;
var y = row * frameHeight + frameHeight / 2;
// Temporarily set animation state
awc.state.animation = frameData.animation;
awc.state.frame = frameData.frame;
// Render
render(awc, ctx, x, y, { scale: scale, time: 0 });
frames.push({
animation: frameData.animation,
frame: frameData.frame,
x: col * frameWidth,
y: row * frameHeight,
width: frameWidth,
height: frameHeight
});
});
return {
canvas: canvas,
frames: frames,
meta: {
width: sheetWidth,
height: sheetHeight,
frameWidth: frameWidth,
frameHeight: frameHeight,
animations: animations
}
};
}
/**
* Export avatar with context for game use
* @param {Object} avatarWithContext - The avatar+context object
* @returns {Object} Exportable data
*/
function exportForGame(avatarWithContext) {
if (!avatarWithContext) return null;
var awc = avatarWithContext;
var context = awc.context;
return {
avatarId: awc.avatar.id || null,
contextId: context.id,
mode: awc.mode,
category: context.category,
animations: context.animations || [],
defaultAnimation: context.defaultAnimation || 'idle',
dimensions: {
width: context.width || 64,
height: context.height || 64
},
avatarPosition: context.avatarPosition || { x: 0.5, y: 0.5 },
avatarScale: context.avatarScale || 1,
accessoryVisibility: context.accessoryVisibility || {},
customState: Object.assign({}, awc.state.custom)
};
}
// ============================================================
// CATALOG ACCESS (delegates to ContextCatalog)
// ============================================================
function getCatalog() {
if (window.ContextCatalog && window.ContextCatalog.getAll) {
return window.ContextCatalog.getAll();
}
// Fallback: aggregate from individual modules
var catalog = [];
if (window.VehicleContexts) {
Object.keys(window.VehicleContexts).forEach(function(id) {
if (typeof window.VehicleContexts[id] === 'object') {
catalog.push(window.VehicleContexts[id]);
}
});
}
if (window.CostumeContexts) {
Object.keys(window.CostumeContexts).forEach(function(id) {
if (typeof window.CostumeContexts[id] === 'object') {
catalog.push(window.CostumeContexts[id]);
}
});
}
if (window.PureContexts) {
Object.keys(window.PureContexts).forEach(function(id) {
if (typeof window.PureContexts[id] === 'object') {
catalog.push(window.PureContexts[id]);
}
});
}
return catalog;
}
function getContext(id) {
return getContextById(id);
}
// ============================================================
// PUBLIC API
// ============================================================
window.ContextRenderer = {
// Core methods
apply: apply,
render: render,
animate: animate,
updateAnimation: updateAnimation,
// Effect control
triggerEffect: triggerEffect,
updateEffects: updateEffects,
clearEffects: clearEffects,
// State management
setState: setState,
getState: getState,
setProp: setProp,
getProp: getProp,
// Context queries
getRequiredMode: getRequiredMode,
getAvailableAnimations: getAvailableAnimations,
// Catalog access (delegates to ContextCatalog)
getCatalog: getCatalog,
getContext: getContext,
// Utility
createSpriteSheet: createSpriteSheet,
exportForGame: exportForGame,
// Version
version: '1.0.0'
};
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,197 @@
// backgroundCatalog.js — Metadata catalog for all 120 procedural backgrounds
// Loaded after all theme files; provides queryable catalog of backgrounds
// 15 themes × 8 variants = 120 unique backgrounds
(function(engine) {
if (!engine) return;
// ─── Catalog Metadata ──────────────────────────────────────
//
// The catalog is auto-generated from theme registrations.
// Each background has:
// id - Unique identifier (theme_variant)
// theme - Parent theme name
// variant - Variant name within theme
// mood - Emotional tone (wonder, tense, serene, energetic, etc.)
// colors - Default color palette [4 hex colors]
// tags - Descriptive tags for filtering
// description - Human-readable description
// compatibleMusicMoods - Array of music moods that pair well
// recommendedFor - Array of game genres this works for
// ─── Theme → Variant Listing ───────────────────────────────
var THEME_VARIANTS = {
space: ['nebula', 'asteroidField', 'deepSpace', 'planets', 'spaceStation', 'warpSpeed', 'satellites', 'blackHole'],
nature: ['forest', 'jungle', 'desert', 'mountains', 'ocean', 'underwater', 'waterfall', 'meadow'],
urban: ['citySkyline', 'street', 'rooftop', 'neonDistrict', 'subway', 'park', 'mall', 'alley'],
fantasy: ['castle', 'dungeon', 'enchantedForest', 'floatingIslands', 'magicalLibrary', 'crystalCave', 'temple', 'tower'],
abstract: ['geometricPatterns', 'gradients', 'particleFields', 'minimalist', 'waves', 'fractals', 'kaleidoscope', 'matrix'],
retro: ['pixelGrid', 'arcadeCabinet', 'crtEffects', 'vaporwave', 'scanlines', 'glitch', 'eightBit', 'synthwave'],
indoor: ['laboratory', 'classroom', 'bedroom', 'kitchen', 'arcade', 'office', 'library', 'gym'],
weather: ['rain', 'snow', 'storm', 'sunset', 'sunrise', 'fog', 'lightning', 'aurora'],
sports: ['stadium', 'raceTrack', 'court', 'field', 'arena', 'pool', 'gym', 'skatepark'],
seasonal: ['springMeadow', 'autumnLeaves', 'winterWonderland', 'summerBeach', 'harvest', 'blossom', 'snowfall', 'tropical'],
underwater: ['coralReef', 'deepSea', 'kelpForest', 'submarine', 'shipwreck', 'abyss', 'iceCave', 'treasure'],
sky: ['clouds', 'flyingHigh', 'hotAirBalloon', 'airplaneView', 'stratosphere', 'birdsEye', 'horizon', 'starfield'],
mechanical: ['gears', 'circuits', 'factory', 'robotFactory', 'conveyorBelts', 'pipes', 'steampunk', 'techLab'],
spooky: ['hauntedHouse', 'graveyard', 'darkForest', 'abandonedBuilding', 'crypt', 'manor', 'fog', 'shadows'],
colorful: ['rainbow', 'paintSplatter', 'candyWorld', 'disco', 'neon', 'tieDye', 'gradient', 'prism'],
};
// ─── Mood Categories ───────────────────────────────────────
var MOOD_DESCRIPTIONS = {
wonder: 'Awe-inspiring and magical',
tense: 'Suspenseful and gripping',
serene: 'Calm and peaceful',
energetic: 'Fast-paced and exciting',
focused: 'Clear and concentrated',
playful: 'Fun and lighthearted',
mysterious:'Dark and intriguing',
nostalgic: 'Retro and reminiscent',
cozy: 'Warm and comfortable',
spooky: 'Eerie and unsettling',
epic: 'Grand and dramatic',
dreamy: 'Soft and ethereal',
vibrant: 'Bright and colorful',
industrial:'Mechanical and gritty',
tropical: 'Warm and exotic',
festive: 'Celebratory and joyful',
};
// ─── Game Genre Mappings ───────────────────────────────────
var GENRE_TAGS = {
action: ['fast', 'intense', 'danger', 'combat'],
puzzle: ['calm', 'focused', 'minimal', 'clean'],
racing: ['fast', 'streaks', 'motion', 'speed'],
rpg: ['epic', 'magical', 'adventure', 'story'],
sports: ['stadium', 'field', 'arena', 'competitive'],
creative:['colorful', 'playful', 'artistic', 'open'],
story: ['atmospheric', 'immersive', 'cinematic'],
physics: ['tech', 'mechanical', 'industrial', 'scientific'],
trivia: ['clean', 'simple', 'focused', 'bright'],
music: ['rhythmic', 'neon', 'disco', 'vibrant'],
};
// ─── Public API Extensions ─────────────────────────────────
/**
* Get all themes with their variant counts
*/
engine.getThemeSummary = function() {
var summary = [];
for (var theme in THEME_VARIANTS) {
summary.push({
theme: theme,
variants: THEME_VARIANTS[theme],
count: THEME_VARIANTS[theme].length
});
}
return summary;
};
/**
* Get total background count
*/
engine.getTotalCount = function() {
var count = 0;
for (var theme in THEME_VARIANTS) {
count += THEME_VARIANTS[theme].length;
}
return count;
};
/**
* Get all available moods
*/
engine.getMoods = function() {
return Object.keys(MOOD_DESCRIPTIONS);
};
/**
* Get mood description
*/
engine.getMoodDescription = function(mood) {
return MOOD_DESCRIPTIONS[mood] || null;
};
/**
* Find backgrounds by game genre
*/
engine.findByGenre = function(genre) {
var tags = GENRE_TAGS[genre] || [];
if (!tags.length) return [];
return engine.find({ tags: tags });
};
/**
* Get a random background from a specific theme
*/
engine.getRandomFromTheme = function(theme) {
var variants = THEME_VARIANTS[theme];
if (!variants || !variants.length) return null;
var variant = variants[Math.floor(Math.random() * variants.length)];
return { theme: theme, variant: variant };
};
/**
* Get a random background matching filters
*/
engine.getRandomMatching = function(filters) {
var matches = engine.find(filters);
if (!matches.length) return null;
return matches[Math.floor(Math.random() * matches.length)];
};
/**
* Find backgrounds compatible with a music mood
*/
engine.findByMusicMood = function(musicMood) {
return engine.getCatalog().filter(function(bg) {
return bg.compatibleMusicMoods && bg.compatibleMusicMoods.includes(musicMood);
});
};
/**
* Get backgrounds recommended for a game type
*/
engine.getRecommendedFor = function(gameType) {
return engine.getCatalog().filter(function(bg) {
return bg.recommendedFor && bg.recommendedFor.includes(gameType);
});
};
/**
* Preload/validate all registered themes
* Returns array of any themes with missing variants
*/
engine.validateCatalog = function() {
var issues = [];
for (var theme in THEME_VARIANTS) {
var variants = THEME_VARIANTS[theme];
variants.forEach(function(variant) {
var catalog = engine.getCatalog();
var found = catalog.find(function(bg) {
return bg.theme === theme && bg.variant === variant;
});
if (!found) {
issues.push({ theme: theme, variant: variant, issue: 'missing registration' });
}
});
}
return issues;
};
// ─── Catalog Statistics ────────────────────────────────────
engine.catalogStats = {
themes: Object.keys(THEME_VARIANTS).length,
variantsPerTheme: 8,
totalBackgrounds: 120,
version: '1.0.0',
lastUpdated: '2026-02-05'
};
})(window.BackgroundEngine);
@@ -0,0 +1,745 @@
// backgroundEngine.js — Procedural background rendering engine
// Loaded by HTML5 games via <script> tag; uses canvas, SVG, and CSS
(function() {
'use strict';
// ─── Helpers ─────────────────────────────────────────────
function lerp(a, b, t) { return a + (b - a) * t; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function rand(min, max) { return Math.random() * (max - min) + min; }
function randInt(min, max) { return Math.floor(rand(min, max + 1)); }
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return { r, g, b };
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(c => clamp(Math.round(c), 0, 255).toString(16).padStart(2, '0')).join('');
}
function blendColor(hex1, hex2, t) {
const a = hexToRgb(hex1), b = hexToRgb(hex2);
return rgbToHex(lerp(a.r, b.r, t), lerp(a.g, b.g, t), lerp(a.b, b.b, t));
}
function lighten(hex, amount) {
const c = hexToRgb(hex);
return rgbToHex(c.r + (255 - c.r) * amount, c.g + (255 - c.g) * amount, c.b + (255 - c.b) * amount);
}
function darken(hex, amount) {
const c = hexToRgb(hex);
return rgbToHex(c.r * (1 - amount), c.g * (1 - amount), c.b * (1 - amount));
}
function rgba(hex, alpha) {
const c = hexToRgb(hex);
return `rgba(${c.r},${c.g},${c.b},${alpha})`;
}
// Seeded pseudo-random for deterministic backgrounds
function seededRandom(seed) {
let s = seed;
return function() {
s = (s * 16807 + 0) % 2147483647;
return (s - 1) / 2147483646;
};
}
// ─── Drawing Primitives ──────────────────────────────────
const Draw = {
linearGradient(ctx, x1, y1, x2, y2, stops) {
const g = ctx.createLinearGradient(x1, y1, x2, y2);
stops.forEach(([pos, color]) => g.addColorStop(pos, color));
return g;
},
radialGradient(ctx, cx, cy, r, stops) {
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
stops.forEach(([pos, color]) => g.addColorStop(pos, color));
return g;
},
fillGradient(ctx, w, h, colors, angle) {
angle = angle || 0;
const rad = angle * Math.PI / 180;
const dx = Math.cos(rad) * w;
const dy = Math.sin(rad) * h;
const cx = w / 2, cy = h / 2;
const stops = colors.map((c, i) => [i / (colors.length - 1), c]);
ctx.fillStyle = Draw.linearGradient(ctx, cx - dx / 2, cy - dy / 2, cx + dx / 2, cy + dy / 2, stops);
ctx.fillRect(0, 0, w, h);
},
mountains(ctx, w, h, baseY, peaks, color, seed) {
const rng = seededRandom(seed || 42);
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, h);
const step = w / peaks;
for (let i = 0; i <= peaks; i++) {
const x = i * step;
const peakH = baseY - rng() * (h * 0.3) - h * 0.05;
if (i === 0) { ctx.lineTo(0, peakH); }
else {
const cpx = x - step / 2;
const cpy = peakH - rng() * (h * 0.1);
ctx.quadraticCurveTo(cpx, cpy, x, baseY - rng() * (h * 0.15));
}
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
},
hills(ctx, w, h, baseY, count, color, seed) {
const rng = seededRandom(seed || 99);
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(0, h);
ctx.lineTo(0, baseY);
for (let i = 0; i < count; i++) {
const x1 = (i / count) * w;
const x2 = ((i + 1) / count) * w;
const cpx = (x1 + x2) / 2;
const cpy = baseY - rng() * h * 0.15 - h * 0.03;
ctx.quadraticCurveTo(cpx, cpy, x2, baseY + rng() * h * 0.02);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
},
stars(ctx, w, h, count, color, seed, sizeRange) {
const rng = seededRandom(seed || 7);
const minS = (sizeRange && sizeRange[0]) || 0.5;
const maxS = (sizeRange && sizeRange[1]) || 2.5;
for (let i = 0; i < count; i++) {
const x = rng() * w;
const y = rng() * h;
const r = minS + rng() * (maxS - minS);
const alpha = 0.4 + rng() * 0.6;
ctx.fillStyle = rgba(color, alpha);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
},
clouds(ctx, w, h, y, count, color, seed) {
const rng = seededRandom(seed || 33);
for (let i = 0; i < count; i++) {
const cx = rng() * w;
const cy = y + rng() * h * 0.15 - h * 0.075;
const cw = 60 + rng() * 120;
const ch = 20 + rng() * 30;
ctx.fillStyle = rgba(color, 0.15 + rng() * 0.25);
ctx.beginPath();
ctx.ellipse(cx, cy, cw, ch, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx - cw * 0.3, cy + ch * 0.2, cw * 0.6, ch * 0.8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + cw * 0.3, cy + ch * 0.1, cw * 0.5, ch * 0.7, 0, 0, Math.PI * 2);
ctx.fill();
}
},
trees(ctx, w, h, baseY, count, trunkColor, leafColor, seed) {
const rng = seededRandom(seed || 55);
for (let i = 0; i < count; i++) {
const x = rng() * w;
const treeH = 30 + rng() * 60;
const trunkW = 3 + rng() * 5;
// Trunk
ctx.fillStyle = trunkColor;
ctx.fillRect(x - trunkW / 2, baseY - treeH, trunkW, treeH * 0.5);
// Canopy
ctx.fillStyle = leafColor;
ctx.beginPath();
ctx.arc(x, baseY - treeH * 0.7, treeH * 0.35, 0, Math.PI * 2);
ctx.fill();
}
},
buildings(ctx, w, h, baseY, count, colors, seed) {
const rng = seededRandom(seed || 88);
const step = w / count;
for (let i = 0; i < count; i++) {
const bw = step * (0.5 + rng() * 0.4);
const bh = 40 + rng() * 120;
const x = i * step + (step - bw) / 2;
const y = baseY - bh;
const color = colors[Math.floor(rng() * colors.length)];
ctx.fillStyle = color;
ctx.fillRect(x, y, bw, bh);
// Windows
const winColor = rgba('#FFFF88', 0.3 + rng() * 0.5);
const cols = Math.floor(bw / 12);
const rows = Math.floor(bh / 15);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (rng() > 0.4) {
ctx.fillStyle = winColor;
ctx.fillRect(x + 4 + c * 12, y + 5 + r * 15, 6, 8);
}
}
}
}
},
water(ctx, w, h, y, color, waveAmp, seed) {
const rng = seededRandom(seed || 44);
ctx.fillStyle = color;
ctx.fillRect(0, y, w, h - y);
// Wave highlights
for (let i = 0; i < 8; i++) {
const wy = y + i * ((h - y) / 8);
ctx.strokeStyle = rgba('#FFFFFF', 0.05 + rng() * 0.08);
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x < w; x += 4) {
const yy = wy + Math.sin(x * 0.02 + i) * waveAmp;
x === 0 ? ctx.moveTo(x, yy) : ctx.lineTo(x, yy);
}
ctx.stroke();
}
},
grid(ctx, w, h, spacing, color, lineWidth) {
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth || 1;
for (let x = 0; x <= w; x += spacing) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
}
for (let y = 0; y <= h; y += spacing) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
}
},
circle(ctx, x, y, r, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
},
rect(ctx, x, y, w, h, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
},
polygon(ctx, cx, cy, r, sides, color, rotation) {
ctx.fillStyle = color;
ctx.beginPath();
for (let i = 0; i < sides; i++) {
const angle = (i / sides) * Math.PI * 2 + (rotation || 0);
const x = cx + Math.cos(angle) * r;
const y = cy + Math.sin(angle) * r;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
},
glow(ctx, x, y, r, color) {
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
g.addColorStop(0, rgba(color, 0.6));
g.addColorStop(0.5, rgba(color, 0.2));
g.addColorStop(1, rgba(color, 0));
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
},
scanlines(ctx, w, h, spacing, color) {
ctx.fillStyle = color;
for (let y = 0; y < h; y += spacing) {
ctx.fillRect(0, y, w, 1);
}
},
};
// ─── Effect System ───────────────────────────────────────
class EffectLayer {
constructor(type, config) {
this.type = type;
this.config = config;
this.particles = [];
this._init();
}
_init() {
const c = this.config;
const count = c.count || 50;
for (let i = 0; i < count; i++) {
this.particles.push(this._createParticle(true));
}
}
_createParticle(initial) {
const c = this.config;
const w = c.width || 800;
const h = c.height || 600;
switch (this.type) {
case 'stars': return {
x: Math.random() * w, y: Math.random() * h,
size: 0.5 + Math.random() * 2, alpha: Math.random(),
twinkleSpeed: 0.5 + Math.random() * 2, phase: Math.random() * Math.PI * 2,
};
case 'rain': return {
x: Math.random() * w, y: initial ? Math.random() * h : -10,
speed: 4 + Math.random() * 8, length: 8 + Math.random() * 15,
alpha: 0.2 + Math.random() * 0.4,
};
case 'snow': return {
x: Math.random() * w, y: initial ? Math.random() * h : -5,
speed: 0.5 + Math.random() * 2, size: 1 + Math.random() * 3,
wobble: Math.random() * Math.PI * 2, wobbleSpeed: 0.5 + Math.random(),
alpha: 0.4 + Math.random() * 0.5,
};
case 'particles': return {
x: Math.random() * w, y: Math.random() * h,
vx: (Math.random() - 0.5) * 1, vy: (Math.random() - 0.5) * 1,
size: 1 + Math.random() * 3, alpha: 0.2 + Math.random() * 0.5,
life: Math.random(),
};
case 'fog': return {
x: Math.random() * w * 1.5 - w * 0.25, y: Math.random() * h,
size: 80 + Math.random() * 160, alpha: 0.03 + Math.random() * 0.08,
speed: 0.2 + Math.random() * 0.5,
};
case 'bubbles': return {
x: Math.random() * w, y: initial ? Math.random() * h : h + 10,
speed: 0.3 + Math.random() * 1.5, size: 2 + Math.random() * 8,
wobble: Math.random() * Math.PI * 2, alpha: 0.2 + Math.random() * 0.4,
};
case 'leaves': return {
x: initial ? Math.random() * w : w + 20, y: Math.random() * h,
speed: 0.5 + Math.random() * 2, size: 4 + Math.random() * 8,
rotation: Math.random() * Math.PI * 2, rotSpeed: (Math.random() - 0.5) * 0.05,
vy: 0.3 + Math.random() * 1, alpha: 0.5 + Math.random() * 0.4,
};
case 'sparkles': return {
x: Math.random() * w, y: Math.random() * h,
size: 1 + Math.random() * 2, alpha: Math.random(),
life: Math.random(), speed: 0.01 + Math.random() * 0.03,
};
case 'fireflies': return {
x: Math.random() * w, y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.5, vy: (Math.random() - 0.5) * 0.5,
size: 2 + Math.random() * 3, phase: Math.random() * Math.PI * 2,
pulseSpeed: 0.5 + Math.random() * 1.5,
};
case 'dust': return {
x: Math.random() * w, y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.3, vy: -0.1 - Math.random() * 0.3,
size: 0.5 + Math.random() * 1.5, alpha: 0.1 + Math.random() * 0.3,
};
default: return { x: 0, y: 0 };
}
}
update(dt, t) {
const c = this.config;
const w = c.width || 800;
const h = c.height || 600;
const speed = c.speed || 1;
this.particles.forEach((p, i) => {
switch (this.type) {
case 'stars':
p.alpha = 0.3 + Math.sin(t * p.twinkleSpeed + p.phase) * 0.35 + 0.35;
break;
case 'rain':
p.y += p.speed * speed * dt * 60;
p.x -= 1 * speed * dt * 60;
if (p.y > h) { this.particles[i] = this._createParticle(false); }
break;
case 'snow':
p.y += p.speed * speed * dt * 60;
p.wobble += p.wobbleSpeed * dt;
p.x += Math.sin(p.wobble) * 0.5;
if (p.y > h + 5) { this.particles[i] = this._createParticle(false); }
break;
case 'particles':
p.x += p.vx * speed * dt * 60;
p.y += p.vy * speed * dt * 60;
p.life += 0.003 * dt * 60;
if (p.life > 1) { this.particles[i] = this._createParticle(false); }
if (p.x < 0 || p.x > w) p.vx *= -1;
if (p.y < 0 || p.y > h) p.vy *= -1;
break;
case 'fog':
p.x += p.speed * speed * dt * 60;
if (p.x > w + p.size) p.x = -p.size;
break;
case 'bubbles':
p.y -= p.speed * speed * dt * 60;
p.wobble += 0.02 * dt * 60;
p.x += Math.sin(p.wobble) * 0.3;
if (p.y < -10) { this.particles[i] = this._createParticle(false); }
break;
case 'leaves':
p.x -= p.speed * speed * dt * 60;
p.y += p.vy * speed * dt * 60;
p.rotation += p.rotSpeed * dt * 60;
if (p.x < -20 || p.y > h + 20) { this.particles[i] = this._createParticle(false); }
break;
case 'sparkles':
p.life += p.speed * dt * 60;
p.alpha = Math.sin(p.life * Math.PI);
if (p.life > 1) { this.particles[i] = this._createParticle(false); }
break;
case 'fireflies':
p.x += p.vx * speed * dt * 60;
p.y += p.vy * speed * dt * 60;
if (Math.random() < 0.01) { p.vx = (Math.random() - 0.5) * 0.5; p.vy = (Math.random() - 0.5) * 0.5; }
p.x = (p.x + w) % w;
p.y = (p.y + h) % h;
break;
case 'dust':
p.x += p.vx * speed * dt * 60;
p.y += p.vy * speed * dt * 60;
if (p.y < -5 || p.x < -5 || p.x > w + 5) { this.particles[i] = this._createParticle(false); }
break;
}
});
}
render(ctx, t) {
const c = this.config;
const color = c.color || '#FFFFFF';
this.particles.forEach(p => {
switch (this.type) {
case 'stars':
ctx.fillStyle = rgba(color, p.alpha);
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
break;
case 'rain':
ctx.strokeStyle = rgba(color, p.alpha);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - 1, p.y + p.length); ctx.stroke();
break;
case 'snow':
ctx.fillStyle = rgba(color, p.alpha);
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
break;
case 'particles':
ctx.fillStyle = rgba(color, p.alpha * (1 - p.life));
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
break;
case 'fog':
ctx.fillStyle = rgba(color, p.alpha);
ctx.beginPath(); ctx.ellipse(p.x, p.y, p.size, p.size * 0.4, 0, 0, Math.PI * 2); ctx.fill();
break;
case 'bubbles':
ctx.strokeStyle = rgba(color, p.alpha);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.stroke();
ctx.fillStyle = rgba(color, p.alpha * 0.2);
ctx.fill();
break;
case 'leaves':
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.fillStyle = rgba(color, p.alpha);
ctx.beginPath(); ctx.ellipse(0, 0, p.size, p.size * 0.4, 0, 0, Math.PI * 2); ctx.fill();
ctx.restore();
break;
case 'sparkles':
if (p.alpha > 0.05) {
ctx.fillStyle = rgba(color, p.alpha);
const s = p.size * p.alpha;
ctx.fillRect(p.x - s, p.y - 0.5, s * 2, 1);
ctx.fillRect(p.x - 0.5, p.y - s, 1, s * 2);
}
break;
case 'fireflies':
const glow = 0.3 + Math.sin(t * p.pulseSpeed + p.phase) * 0.35 + 0.35;
Draw.glow(ctx, p.x, p.y, p.size * 3, color);
ctx.fillStyle = rgba(color, glow);
ctx.beginPath(); ctx.arc(p.x, p.y, p.size * 0.5, 0, Math.PI * 2); ctx.fill();
break;
case 'dust':
ctx.fillStyle = rgba(color, p.alpha);
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
break;
}
});
}
}
// ─── BackgroundEngine ────────────────────────────────────
class BackgroundEngine {
constructor() {
this._themes = {};
this._canvas = null;
this._ctx = null;
this._running = false;
this._animId = null;
this._currentTheme = null;
this._currentVariant = null;
this._effects = [];
this._parallax = { enabled: false, layers: 1, offsetX: 0, offsetY: 0 };
this._colors = null;
this._animSpeed = 1;
this._lastTime = 0;
this._startTime = 0;
this._bgCache = null;
}
registerTheme(name, variants) {
this._themes[name] = variants;
}
load(theme, variant, options) {
options = options || {};
const themeData = this._themes[theme];
if (!themeData) { console.warn('BackgroundEngine: unknown theme "' + theme + '"'); return false; }
const varData = themeData[variant];
if (!varData) { console.warn('BackgroundEngine: unknown variant "' + variant + '" in theme "' + theme + '"'); return false; }
// Setup canvas
if (options.canvas) {
this._canvas = options.canvas;
} else if (!this._canvas) {
this._canvas = document.createElement('canvas');
this._canvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;pointer-events:none;';
document.body.insertBefore(this._canvas, document.body.firstChild);
}
this._canvas.width = options.width || this._canvas.clientWidth || window.innerWidth;
this._canvas.height = options.height || this._canvas.clientHeight || window.innerHeight;
this._ctx = this._canvas.getContext('2d');
this._currentTheme = theme;
this._currentVariant = variant;
this._colors = options.colors || varData.colors || null;
this._effects = [];
this._bgCache = null;
this._startTime = performance.now() / 1000;
// Add default effects from theme
if (varData.particles) {
this.addEffect(varData.particles.type, varData.particles);
}
// Render initial static frame
this._renderStatic();
// Start animation loop
if (!this._running) {
this._running = true;
this._lastTime = performance.now() / 1000;
this._loop();
}
return true;
}
addEffect(effectType, config) {
config = config || {};
config.width = this._canvas ? this._canvas.width : 800;
config.height = this._canvas ? this._canvas.height : 600;
config.speed = config.speed || this._animSpeed;
this._effects.push(new EffectLayer(effectType, config));
}
setParallax(layers) {
this._parallax.enabled = true;
this._parallax.layers = layers || 3;
// Listen for mouse/touch movement
const self = this;
const handler = function(e) {
const x = e.clientX || (e.touches && e.touches[0] && e.touches[0].clientX) || 0;
const y = e.clientY || (e.touches && e.touches[0] && e.touches[0].clientY) || 0;
const w = window.innerWidth, h = window.innerHeight;
self._parallax.offsetX = (x / w - 0.5) * 30;
self._parallax.offsetY = (y / h - 0.5) * 15;
};
window.addEventListener('mousemove', handler);
window.addEventListener('touchmove', handler);
this._parallax._cleanup = function() {
window.removeEventListener('mousemove', handler);
window.removeEventListener('touchmove', handler);
};
this._bgCache = null;
}
updateColors(palette) {
this._colors = palette;
this._bgCache = null;
this._renderStatic();
}
animate(speed) {
this._animSpeed = clamp(speed, 0, 5);
this._effects.forEach(e => { e.config.speed = this._animSpeed; });
}
stop() {
this._running = false;
if (this._animId) cancelAnimationFrame(this._animId);
this._effects = [];
if (this._parallax._cleanup) this._parallax._cleanup();
this._parallax = { enabled: false, layers: 1, offsetX: 0, offsetY: 0 };
}
getCatalog() {
const catalog = [];
for (const [themeName, variants] of Object.entries(this._themes)) {
for (const [varName, varData] of Object.entries(variants)) {
if (varData.meta) {
catalog.push({ ...varData.meta, theme: themeName, variant: varName });
}
}
}
return catalog;
}
find(filters) {
filters = filters || {};
return this.getCatalog().filter(item => {
if (filters.theme && item.theme !== filters.theme) return false;
if (filters.mood && item.mood !== filters.mood) return false;
if (filters.tags && !filters.tags.some(t => item.tags && item.tags.includes(t))) return false;
return true;
});
}
getThemes() {
return Object.keys(this._themes);
}
getVariants(theme) {
return this._themes[theme] ? Object.keys(this._themes[theme]) : [];
}
// ─── Internal ────────────────────────────────────
_getVariantData() {
if (!this._currentTheme || !this._currentVariant) return null;
return this._themes[this._currentTheme] && this._themes[this._currentTheme][this._currentVariant];
}
_getColors() {
const varData = this._getVariantData();
return this._colors || (varData && varData.colors) || ['#1a1a2e', '#16213e', '#0f3460'];
}
_renderStatic() {
const varData = this._getVariantData();
if (!varData || !this._ctx) return;
const ctx = this._ctx;
const w = this._canvas.width;
const h = this._canvas.height;
const colors = this._getColors();
if (varData.renderBase) {
varData.renderBase(ctx, w, h, colors, Draw, 0);
}
if (varData.renderMid) {
varData.renderMid(ctx, w, h, colors, Draw, 0);
}
}
_loop() {
if (!this._running) return;
const now = performance.now() / 1000;
const dt = Math.min(now - this._lastTime, 0.1);
const t = now - this._startTime;
this._lastTime = now;
const ctx = this._ctx;
const w = this._canvas.width;
const h = this._canvas.height;
const varData = this._getVariantData();
const colors = this._getColors();
if (varData) {
// Re-render base (may be animated)
ctx.clearRect(0, 0, w, h);
if (this._parallax.enabled) {
const px = this._parallax.offsetX;
const py = this._parallax.offsetY;
// Base layer (no parallax)
if (varData.renderBase) varData.renderBase(ctx, w, h, colors, Draw, t);
// Mid layer (slight parallax)
ctx.save();
ctx.translate(px * 0.5, py * 0.5);
if (varData.renderMid) varData.renderMid(ctx, w, h, colors, Draw, t);
ctx.restore();
// Effects layer (full parallax)
ctx.save();
ctx.translate(px, py);
this._effects.forEach(e => { e.update(dt, t); e.render(ctx, t); });
ctx.restore();
} else {
if (varData.renderBase) varData.renderBase(ctx, w, h, colors, Draw, t);
if (varData.renderMid) varData.renderMid(ctx, w, h, colors, Draw, t);
this._effects.forEach(e => { e.update(dt, t); e.render(ctx, t); });
}
}
this._animId = requestAnimationFrame(() => this._loop());
}
}
// Expose globally
const engine = new BackgroundEngine();
// Expose Draw utilities for theme files
engine.Draw = Draw;
engine.helpers = { lerp, clamp, rand, randInt, pick, hexToRgb, rgbToHex, blendColor, lighten, darken, rgba, seededRandom };
/**
* Static-like render method for one-shot rendering to any canvas
* @param {CanvasRenderingContext2D} ctx - Canvas context to render to
* @param {string} theme - Theme name (e.g., 'space', 'nature')
* @param {string} variant - Variant name (e.g., 'nebula', 'forest')
* @param {number} w - Canvas width
* @param {number} h - Canvas height
* @param {number} time - Animation time in ms (0 for static)
* @param {string[]} [customColors] - Optional custom color palette
*/
engine.render = function(ctx, theme, variant, w, h, time, customColors) {
const varData = this._themes[theme] && this._themes[theme][variant];
if (!varData) {
// Fallback: draw a simple gradient
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, w, h);
return;
}
const colors = customColors || varData.colors || ['#1a1a2e', '#16213e', '#0f3460', '#e94560'];
const t = (time || 0) / 1000; // Convert ms to seconds
// Clear and render
ctx.clearRect(0, 0, w, h);
if (varData.renderBase) {
varData.renderBase(ctx, w, h, colors, Draw, t);
}
if (varData.renderMid) {
varData.renderMid(ctx, w, h, colors, Draw, t);
}
};
window.BackgroundEngine = engine;
})();
+47
View File
@@ -0,0 +1,47 @@
// effects.js — Shared effect presets for background system
// Depends on backgroundEngine.js being loaded first
(function(engine) {
if (!engine) return;
// Pre-configured effect presets that themes can reference by name
engine.effectPresets = {
starfield: { type: 'stars', count: 120, color: '#FFFFFF' },
starfieldDense:{ type: 'stars', count: 250, color: '#FFFFFF' },
starfieldSparse:{ type: 'stars', count: 40, color: '#FFFFFF' },
lightRain: { type: 'rain', count: 60, color: '#AACCEE' },
heavyRain: { type: 'rain', count: 200, color: '#8899BB' },
lightSnow: { type: 'snow', count: 40, color: '#FFFFFF' },
heavySnow: { type: 'snow', count: 150, color: '#E8E8F0' },
floatingDust: { type: 'dust', count: 30, color: '#DDDDAA' },
embers: { type: 'particles', count: 25, color: '#FF8844' },
sparkle: { type: 'sparkles', count: 30, color: '#FFFFFF' },
goldSparkle: { type: 'sparkles', count: 30, color: '#FFD700' },
thinFog: { type: 'fog', count: 8, color: '#CCCCCC' },
thickFog: { type: 'fog', count: 20, color: '#999999' },
darkFog: { type: 'fog', count: 15, color: '#333344' },
bubbles: { type: 'bubbles', count: 25, color: '#88CCEE' },
denseBubbles: { type: 'bubbles', count: 60, color: '#66AADD' },
fallingLeaves:{ type: 'leaves', count: 15, color: '#CC8833' },
autumnLeaves: { type: 'leaves', count: 30, color: '#DD6622' },
cherryBlossom:{ type: 'leaves', count: 25, color: '#FFB7C5' },
fireflies: { type: 'fireflies', count: 15, color: '#CCFF44' },
denseFireflies:{ type: 'fireflies',count: 35, color: '#AAEE33' },
magicParticles:{ type: 'particles',count: 40, color: '#BB77FF' },
neonParticles:{ type: 'particles', count: 50, color: '#00FFCC' },
smokeRising: { type: 'dust', count: 20, color: '#888888' },
snowfall: { type: 'snow', count: 80, color: '#FFFFFF' },
rainStorm: { type: 'rain', count: 300, color: '#667799' },
};
// Helper to apply a preset by name
engine.addPresetEffect = function(presetName) {
const preset = engine.effectPresets[presetName];
if (preset) {
engine.addEffect(preset.type, { ...preset });
} else {
console.warn('BackgroundEngine: unknown effect preset "' + presetName + '"');
}
};
})(window.BackgroundEngine);
@@ -0,0 +1,396 @@
// abstract.js — Abstract theme backgrounds
// Variants: geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('abstract', {
geometricPatterns: {
meta: { id: 'abstract_geometricPatterns', mood: 'focused', colors: ['#0d1b2a', '#1b2838', '#e07a5f', '#f2cc8f'], tags: ['geometric', 'shapes', 'patterns', 'clean'], description: 'Interlocking geometric shapes in warm contrasting tones on a deep navy canvas', compatibleMusicMoods: ['focused', 'calm', 'electronic'], recommendedFor: ['puzzle', 'trivia', 'creative'] },
colors: ['#0d1b2a', '#1b2838', '#e07a5f', '#f2cc8f'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 135);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(100);
// Rotating hexagonal grid
var spacing = 80;
for (var row = -1; row < h / spacing + 1; row++) {
for (var col = -1; col < w / spacing + 1; col++) {
var hx = col * spacing + (row % 2 === 0 ? spacing / 2 : 0);
var hy = row * spacing;
var dist = Math.sqrt(Math.pow(hx - w / 2, 2) + Math.pow(hy - h / 2, 2));
var alpha = H.clamp(0.08 + Math.sin(t * 0.5 + dist * 0.005) * 0.06, 0, 0.2);
var rot = t * 0.1 + dist * 0.002;
var colorChoice = rng() > 0.5 ? c[2] : c[3];
Draw.polygon(ctx, hx, hy, 25 + Math.sin(t * 0.3 + dist * 0.01) * 5, 6, H.rgba(colorChoice, alpha), rot);
}
}
// Accent triangles
for (var tri = 0; tri < 12; tri++) {
var tx = rng() * w, ty = rng() * h;
var tSize = 20 + rng() * 40;
var tRot = t * 0.2 * (rng() > 0.5 ? 1 : -1) + rng() * Math.PI * 2;
var tAlpha = 0.1 + Math.sin(t * 0.7 + tri) * 0.05;
Draw.polygon(ctx, tx, ty, tSize, 3, H.rgba(c[2], tAlpha), tRot);
}
// Connecting lines
ctx.strokeStyle = H.rgba(c[3], 0.06);
ctx.lineWidth = 1;
var lrng = H.seededRandom(101);
for (var ln = 0; ln < 15; ln++) {
ctx.beginPath();
ctx.moveTo(lrng() * w, lrng() * h);
ctx.lineTo(lrng() * w, lrng() * h);
ctx.stroke();
}
},
particles: { type: 'dust', count: 20, color: '#F2CC8F' }
},
gradients: {
meta: { id: 'abstract_gradients', mood: 'dreamy', colors: ['#0f0c29', '#302b63', '#24243e', '#ff6b6b'], tags: ['gradient', 'smooth', 'colorful', 'flowing'], description: 'Smoothly blending gradient orbs that shift and pulse with ethereal color transitions', compatibleMusicMoods: ['dreamy', 'calm', 'ambient'], recommendedFor: ['creative', 'story', 'music'] },
colors: ['#0f0c29', '#302b63', '#24243e', '#ff6b6b'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(200);
// Animated gradient orbs
var orbs = [
{ x: 0.3, y: 0.3, color: c[3], r: 0.25 },
{ x: 0.7, y: 0.6, color: '#6b5bff', r: 0.3 },
{ x: 0.5, y: 0.8, color: '#ff9a76', r: 0.2 },
{ x: 0.2, y: 0.7, color: '#a66bff', r: 0.22 },
{ x: 0.8, y: 0.2, color: '#ff6bb5', r: 0.18 }
];
ctx.globalCompositeOperation = 'screen';
for (var i = 0; i < orbs.length; i++) {
var orb = orbs[i];
var ox = orb.x * w + Math.sin(t * 0.3 + i * 1.5) * w * 0.05;
var oy = orb.y * h + Math.cos(t * 0.2 + i * 1.2) * h * 0.05;
var or = orb.r * Math.min(w, h) * (1 + Math.sin(t * 0.4 + i) * 0.1);
var grad = Draw.radialGradient(ctx, ox, oy, or, [
[0, H.rgba(orb.color, 0.3)],
[0.5, H.rgba(orb.color, 0.1)],
[1, H.rgba(orb.color, 0)]
]);
ctx.fillStyle = grad;
ctx.beginPath(); ctx.arc(ox, oy, or, 0, Math.PI * 2); ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
// Subtle mesh lines
ctx.strokeStyle = H.rgba('#FFFFFF', 0.02);
ctx.lineWidth = 1;
for (var ml = 0; ml < 8; ml++) {
var my = h * 0.1 + ml * h * 0.1;
ctx.beginPath();
for (var mx = 0; mx < w; mx += 5) {
var mdy = my + Math.sin(mx * 0.01 + t * 0.5 + ml * 0.5) * 15;
mx === 0 ? ctx.moveTo(mx, mdy) : ctx.lineTo(mx, mdy);
}
ctx.stroke();
}
},
particles: { type: 'sparkles', count: 15, color: '#FF6B6B' }
},
particleFields: {
meta: { id: 'abstract_particleFields', mood: 'energetic', colors: ['#000000', '#111122', '#00ff88', '#0088ff'], tags: ['particles', 'field', 'connected', 'network'], description: 'A dynamic network of connected particle nodes with pulsing energy links', compatibleMusicMoods: ['energetic', 'electronic', 'focused'], recommendedFor: ['action', 'puzzle', 'physics'] },
colors: ['#000000', '#111122', '#00ff88', '#0088ff'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(300);
// Generate node positions (deterministic)
var nodes = [];
for (var n = 0; n < 30; n++) {
var baseX = rng() * w, baseY = rng() * h;
nodes.push({
x: baseX + Math.sin(t * 0.5 + n * 0.7) * 15,
y: baseY + Math.cos(t * 0.4 + n * 0.9) * 15,
size: 2 + rng() * 3,
color: rng() > 0.5 ? c[2] : c[3]
});
}
// Draw connections between nearby nodes
var maxDist = 150;
for (var i = 0; i < nodes.length; i++) {
for (var j = i + 1; j < nodes.length; j++) {
var dx = nodes[i].x - nodes[j].x;
var dy = nodes[i].y - nodes[j].y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < maxDist) {
var lineAlpha = (1 - dist / maxDist) * 0.2;
var pulse = 0.5 + Math.sin(t * 1.5 + i + j) * 0.5;
ctx.strokeStyle = H.rgba(nodes[i].color, lineAlpha * pulse);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.stroke();
}
}
}
// Draw nodes
for (var k = 0; k < nodes.length; k++) {
var nd = nodes[k];
var nodePulse = 0.6 + Math.sin(t * 2 + k * 0.5) * 0.3;
Draw.glow(ctx, nd.x, nd.y, nd.size * 4, H.rgba(nd.color, 0.15 * nodePulse));
Draw.circle(ctx, nd.x, nd.y, nd.size, H.rgba(nd.color, 0.7 * nodePulse));
}
// Data stream accents along bottom
ctx.strokeStyle = H.rgba(c[2], 0.04);
ctx.lineWidth = 1;
for (var ds = 0; ds < 5; ds++) {
var dsy = h * 0.85 + ds * 8;
ctx.beginPath();
for (var dx2 = 0; dx2 < w; dx2 += 3) {
var ddy = dsy + Math.sin(dx2 * 0.03 + t * 2 + ds) * 3;
dx2 === 0 ? ctx.moveTo(dx2, ddy) : ctx.lineTo(dx2, ddy);
}
ctx.stroke();
}
},
particles: { type: 'particles', count: 15, color: '#00FF88' }
},
minimalist: {
meta: { id: 'abstract_minimalist', mood: 'calm', colors: ['#f5f5f0', '#e8e4dd', '#2d2d2d', '#c44536'], tags: ['minimal', 'clean', 'simple', 'elegant'], description: 'Clean open space with a single bold accent line and carefully placed geometric forms', compatibleMusicMoods: ['calm', 'ambient', 'focused'], recommendedFor: ['puzzle', 'trivia', 'creative'] },
colors: ['#f5f5f0', '#e8e4dd', '#2d2d2d', '#c44536'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Single horizon line
ctx.strokeStyle = H.rgba(c[2], 0.15);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, h * 0.618); ctx.lineTo(w, h * 0.618); ctx.stroke();
// Accent circle
var circX = w * 0.7, circY = h * 0.35;
var circR = Math.min(w, h) * 0.08;
var breathe = 1 + Math.sin(t * 0.5) * 0.03;
Draw.circle(ctx, circX, circY, circR * breathe, H.rgba(c[3], 0.7));
// Shadow beneath circle
ctx.fillStyle = H.rgba(c[2], 0.04);
ctx.beginPath(); ctx.ellipse(circX, h * 0.618, circR * 1.5, 4, 0, 0, Math.PI * 2); ctx.fill();
// Thin accent line from circle
ctx.strokeStyle = H.rgba(c[3], 0.2);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(circX, circY + circR * breathe);
ctx.lineTo(circX, h * 0.618);
ctx.stroke();
// Sparse dots
var rng = H.seededRandom(400);
for (var d = 0; d < 5; d++) {
var dx = rng() * w, dy = rng() * h;
Draw.circle(ctx, dx, dy, 2, H.rgba(c[2], 0.08));
}
// Small typography-like lines
var lx = w * 0.15;
for (var l = 0; l < 4; l++) {
var lw = 30 + rng() * 60;
ctx.fillStyle = H.rgba(c[2], 0.06);
ctx.fillRect(lx, h * 0.72 + l * 12, lw, 2);
}
},
particles: null
},
waves: {
meta: { id: 'abstract_waves', mood: 'flowing', colors: ['#0a192f', '#172a45', '#00b4d8', '#90e0ef'], tags: ['waves', 'ocean', 'flowing', 'smooth'], description: 'Layered sinusoidal waves undulating in cool blue tones with foam accents', compatibleMusicMoods: ['calm', 'flowing', 'ambient'], recommendedFor: ['creative', 'music', 'physics'] },
colors: ['#0a192f', '#172a45', '#00b4d8', '#90e0ef'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Layered wave bands
var waveLayers = [
{ y: 0.3, amp: 25, freq: 0.008, speed: 0.4, color: c[2], alpha: 0.08 },
{ y: 0.4, amp: 30, freq: 0.006, speed: 0.3, color: c[2], alpha: 0.1 },
{ y: 0.5, amp: 20, freq: 0.01, speed: 0.5, color: c[3], alpha: 0.12 },
{ y: 0.6, amp: 35, freq: 0.005, speed: 0.25, color: c[2], alpha: 0.15 },
{ y: 0.7, amp: 15, freq: 0.012, speed: 0.6, color: c[3], alpha: 0.1 },
{ y: 0.8, amp: 40, freq: 0.004, speed: 0.2, color: c[2], alpha: 0.18 }
];
for (var i = 0; i < waveLayers.length; i++) {
var wl = waveLayers[i];
ctx.fillStyle = H.rgba(wl.color, wl.alpha);
ctx.beginPath();
ctx.moveTo(0, h);
for (var x = 0; x <= w; x += 3) {
var y = wl.y * h + Math.sin(x * wl.freq + t * wl.speed) * wl.amp +
Math.sin(x * wl.freq * 2.3 + t * wl.speed * 1.5) * wl.amp * 0.3;
ctx.lineTo(x, y);
}
ctx.lineTo(w, h);
ctx.closePath();
ctx.fill();
}
// Foam highlights on top wave
ctx.strokeStyle = H.rgba('#FFFFFF', 0.06);
ctx.lineWidth = 1.5;
ctx.beginPath();
for (var fx = 0; fx <= w; fx += 3) {
var fy = 0.3 * h + Math.sin(fx * 0.008 + t * 0.4) * 25;
fx === 0 ? ctx.moveTo(fx, fy) : ctx.lineTo(fx, fy);
}
ctx.stroke();
},
particles: { type: 'bubbles', count: 15, color: '#90E0EF' }
},
fractals: {
meta: { id: 'abstract_fractals', mood: 'complex', colors: ['#0a0a0a', '#1a1a2e', '#e94560', '#0f3460'], tags: ['fractal', 'recursive', 'mathematical', 'complex'], description: 'Recursive branching patterns that grow and pulse like living mathematical organisms', compatibleMusicMoods: ['complex', 'electronic', 'mystical'], recommendedFor: ['puzzle', 'physics', 'creative'] },
colors: ['#0a0a0a', '#1a1a2e', '#e94560', '#0f3460'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Recursive tree fractal
function branch(x, y, len, angle, depth, maxDepth) {
if (depth > maxDepth || len < 3) return;
var endX = x + Math.cos(angle) * len;
var endY = y + Math.sin(angle) * len;
var alpha = 0.15 * (1 - depth / maxDepth) + Math.sin(t * 0.5 + depth) * 0.03;
var lw = (maxDepth - depth) * 0.8 + 0.5;
ctx.strokeStyle = H.rgba(depth % 2 === 0 ? c[2] : c[3], alpha);
ctx.lineWidth = lw;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(endX, endY); ctx.stroke();
var spread = 0.4 + Math.sin(t * 0.3 + depth * 0.5) * 0.1;
branch(endX, endY, len * 0.7, angle - spread, depth + 1, maxDepth);
branch(endX, endY, len * 0.7, angle + spread, depth + 1, maxDepth);
}
// Main tree from bottom center
branch(w * 0.5, h * 0.85, h * 0.18, -Math.PI / 2, 0, 9);
// Smaller trees
branch(w * 0.2, h * 0.9, h * 0.1, -Math.PI / 2, 0, 7);
branch(w * 0.8, h * 0.9, h * 0.12, -Math.PI / 2, 0, 7);
// Spiral accents
var rng = H.seededRandom(500);
for (var sp = 0; sp < 4; sp++) {
var sx = rng() * w, sy = rng() * h * 0.6;
ctx.strokeStyle = H.rgba(c[2], 0.06);
ctx.lineWidth = 1;
ctx.beginPath();
for (var sa = 0; sa < Math.PI * 6; sa += 0.1) {
var sr = sa * 2 + Math.sin(t * 0.3 + sp) * 2;
var ssx = sx + Math.cos(sa) * sr;
var ssy = sy + Math.sin(sa) * sr;
sa === 0 ? ctx.moveTo(ssx, ssy) : ctx.lineTo(ssx, ssy);
}
ctx.stroke();
}
},
particles: { type: 'sparkles', count: 20, color: '#E94560' }
},
kaleidoscope: {
meta: { id: 'abstract_kaleidoscope', mood: 'playful', colors: ['#1a1a2e', '#16213e', '#e94560', '#ffd166'], tags: ['kaleidoscope', 'symmetry', 'colorful', 'playful'], description: 'Symmetric radial patterns in vibrant colors rotating like a living kaleidoscope', compatibleMusicMoods: ['playful', 'energetic', 'electronic'], recommendedFor: ['music', 'creative', 'puzzle'] },
colors: ['#1a1a2e', '#16213e', '#e94560', '#ffd166'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var cx = w / 2, cy = h / 2;
var segments = 8;
var rng = H.seededRandom(600);
ctx.save();
ctx.translate(cx, cy);
// Draw mirrored segments
for (var seg = 0; seg < segments; seg++) {
var segAngle = (seg / segments) * Math.PI * 2 + t * 0.1;
ctx.save();
ctx.rotate(segAngle);
// Shapes within each segment
for (var s = 0; s < 5; s++) {
var sd = 40 + s * 35 + Math.sin(t * 0.4 + s) * 10;
var sSize = 8 + rng() * 15;
var sColor = s % 2 === 0 ? c[2] : c[3];
var sAlpha = 0.12 + Math.sin(t * 0.6 + s * 0.8) * 0.05;
Draw.polygon(ctx, sd, 0, sSize, 3 + (s % 3), H.rgba(sColor, sAlpha), t * 0.2 + s);
}
// Connecting arcs
ctx.strokeStyle = H.rgba(c[2], 0.06);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(0, 0, 80 + Math.sin(t * 0.5) * 10, 0, Math.PI / segments);
ctx.stroke();
ctx.restore();
}
ctx.restore();
// Central glow
var centerPulse = 0.15 + Math.sin(t * 0.8) * 0.05;
Draw.glow(ctx, cx, cy, 60, H.rgba(c[3], centerPulse));
Draw.glow(ctx, cx, cy, 30, H.rgba(c[2], centerPulse * 1.5));
// Outer ring
ctx.strokeStyle = H.rgba(c[3], 0.08 + Math.sin(t * 0.3) * 0.03);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy, Math.min(w, h) * 0.35, 0, Math.PI * 2);
ctx.stroke();
},
particles: { type: 'sparkles', count: 25, color: '#FFD166' }
},
matrix: {
meta: { id: 'abstract_matrix', mood: 'digital', colors: ['#000000', '#001100', '#00ff41', '#008f11'], tags: ['matrix', 'code', 'digital', 'hacker'], description: 'Cascading columns of digital characters streaming down like the matrix rain', compatibleMusicMoods: ['dark', 'electronic', 'tense'], recommendedFor: ['action', 'puzzle', 'trivia'] },
colors: ['#000000', '#001100', '#00ff41', '#008f11'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// CRT vignette
var vignette = Draw.radialGradient(ctx, w / 2, h / 2, Math.max(w, h) * 0.7, [
[0, H.rgba(c[1], 0)],
[0.7, H.rgba(c[0], 0.3)],
[1, H.rgba(c[0], 0.8)]
]);
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, w, h);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(700);
var columns = Math.floor(w / 16);
var chars = '01アイウエオカキクケコサシスセソ{}[]<>+=/*#@$%&';
ctx.font = '13px monospace';
// Column rain
for (var col = 0; col < columns; col++) {
var cx = col * 16 + 4;
var speed = 0.3 + rng() * 0.7;
var colOffset = rng() * h;
var colLen = 8 + Math.floor(rng() * 20);
// Calculate head position
var headY = ((t * speed * 60 + colOffset) % (h + colLen * 16)) - colLen * 16;
for (var ch = 0; ch < colLen; ch++) {
var cy = headY + ch * 16;
if (cy < -16 || cy > h + 16) continue;
var charIdx = Math.floor(rng() * chars.length);
// Head is brightest, tail fades
var fade = 1 - (ch / colLen);
if (ch === 0) {
ctx.fillStyle = H.rgba('#FFFFFF', 0.8);
} else {
ctx.fillStyle = H.rgba(c[2], fade * 0.5 + Math.sin(t * 3 + col + ch) * 0.08);
}
ctx.fillText(chars[charIdx], cx, cy);
}
}
// Occasional bright flashes
if (Math.sin(t * 1.3) > 0.95) {
var flashX = Math.floor(rng() * columns) * 16;
var flashY = rng() * h;
Draw.glow(ctx, flashX, flashY, 30, H.rgba(c[2], 0.3));
}
// Subtle horizontal scan line
var scanY = (t * 40) % h;
ctx.fillStyle = H.rgba(c[2], 0.03);
ctx.fillRect(0, scanY, w, 2);
},
particles: null
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,625 @@
// colorful.js — Colorful/vibrant theme for procedural background system
// 8 variants: rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
// Helper: draw a paint splatter blob
function drawSplatter(ctx, cx, cy, r, color, rng) {
ctx.fillStyle = color;
// Main blob
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
// Splatter drops radiating outward
for (var i = 0; i < 6; i++) {
var angle = rng() * Math.PI * 2;
var dist = r + rng() * r * 1.5;
var dropR = 2 + rng() * r * 0.35;
ctx.beginPath();
ctx.arc(cx + Math.cos(angle) * dist, cy + Math.sin(angle) * dist, dropR, 0, Math.PI * 2);
ctx.fill();
}
// Drip trails
for (var i = 0; i < 2; i++) {
var dx = cx + (rng() - 0.5) * r;
var dLen = r * 0.5 + rng() * r;
ctx.beginPath();
ctx.moveTo(dx - 2, cy + r * 0.5);
ctx.quadraticCurveTo(dx, cy + r * 0.5 + dLen * 0.5, dx - 1, cy + r * 0.5 + dLen);
ctx.lineTo(dx + 2, cy + r * 0.5 + dLen);
ctx.quadraticCurveTo(dx + 1, cy + r * 0.5 + dLen * 0.5, dx + 2, cy + r * 0.5);
ctx.closePath();
ctx.fill();
}
}
// Helper: draw a candy/lollipop
function drawLollipop(ctx, x, baseY, r, colors, rng) {
// Stick
ctx.strokeStyle = '#DDCCBB';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x, baseY);
ctx.lineTo(x, baseY - r * 2.5);
ctx.stroke();
// Swirled candy top
var segments = 6;
for (var i = segments - 1; i >= 0; i--) {
var a1 = (i / segments) * Math.PI * 2;
var a2 = ((i + 1) / segments) * Math.PI * 2;
ctx.fillStyle = colors[i % colors.length];
ctx.beginPath();
ctx.moveTo(x, baseY - r * 2.5);
ctx.arc(x, baseY - r * 2.5, r, a1, a2);
ctx.closePath();
ctx.fill();
}
// Shine
ctx.fillStyle = H.rgba('#FFFFFF', 0.25);
ctx.beginPath();
ctx.arc(x - r * 0.25, baseY - r * 2.5 - r * 0.2, r * 0.3, 0, Math.PI * 2);
ctx.fill();
}
// Helper: draw a disco ball facet pattern
function drawDiscoBall(ctx, cx, cy, r, t) {
// Ball base
var g = D.radialGradient(ctx, cx - r * 0.3, cy - r * 0.3, r, [[0, '#EEEEEE'], [0.5, '#AAAAAA'], [1, '#555555']]);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
// Mirror facets
var rows = 6, cols = 8;
for (var row = 0; row < rows; row++) {
var rowAngle = (row / rows) * Math.PI - Math.PI / 2;
var rowR = Math.cos(rowAngle) * r;
var rowY = cy + Math.sin(rowAngle) * r * 0.85;
for (var col = 0; col < cols; col++) {
var colAngle = (col / cols) * Math.PI * 2 + t * 0.5;
var fx = cx + Math.cos(colAngle) * rowR * 0.85;
var fy = rowY;
var sparkle = 0.2 + Math.sin(t * 3 + row * 2 + col * 1.5) * 0.3;
if (sparkle > 0.1) {
ctx.fillStyle = H.rgba('#FFFFFF', sparkle);
ctx.fillRect(fx - 3, fy - 2, 6, 4);
}
}
}
}
// Rainbow palette constant
var RAINBOW = ['#FF0000', '#FF7700', '#FFDD00', '#00CC44', '#0088FF', '#6633CC', '#CC33AA'];
engine.registerTheme('colorful', {
rainbow: {
meta: { id: 'colorful_rainbow', mood: 'joyful', colors: ['#FF4444', '#FFAA33', '#FFEE33', '#44DD55'], tags: ['rainbow', 'bright', 'cheerful'], description: 'Bold rainbow arcs and bands across a bright sky', compatibleMusicMoods: ['playful', 'energetic', 'dreamy'], recommendedFor: ['creative', 'puzzle', 'music'] },
colors: ['#FF4444', '#FFAA33', '#FFEE33', '#44DD55'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#2244AA', '#4488CC', '#88BBEE'], 90);
// Sun glow
D.glow(ctx, w * 0.85, h * 0.1, 80, H.rgba('#FFEE44', 0.25));
D.circle(ctx, w * 0.85, h * 0.1, 25, H.rgba('#FFEE44', 0.6));
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Rainbow arc
var rcx = w * 0.4, rcy = h * 1.1;
var baseR = Math.min(w, h) * 0.8;
for (var i = 0; i < RAINBOW.length; i++) {
var rr = baseR + i * 12;
ctx.strokeStyle = H.rgba(RAINBOW[i], 0.5);
ctx.lineWidth = 10;
ctx.beginPath();
ctx.arc(rcx, rcy, rr, Math.PI, 0);
ctx.stroke();
}
// Soft clouds
Draw.clouds(ctx, w, h, h * 0.25, 5, '#FFFFFF', 101);
Draw.clouds(ctx, w, h, h * 0.65, 4, '#FFFFFF', 102);
// Color sparkles along rainbow
var rng = H.seededRandom(103);
for (var i = 0; i < 15; i++) {
var sparkAngle = Math.PI - rng() * Math.PI;
var sparkR = baseR + rng() * RAINBOW.length * 12;
var sx = rcx + Math.cos(sparkAngle) * sparkR;
var sy = rcy + Math.sin(sparkAngle) * sparkR;
var twinkle = 0.3 + Math.sin(t * 2 + i * 1.3) * 0.3;
D.circle(ctx, sx, sy, 2, H.rgba(H.pick(RAINBOW), twinkle));
}
// Green hills at bottom
Draw.hills(ctx, w, h, h * 0.85, 6, '#44AA44', 104);
Draw.hills(ctx, w, h, h * 0.9, 5, '#338833', 105);
},
particles: { type: 'sparkles', count: 35, color: '#FFFFFF' }
},
paintSplatter: {
meta: { id: 'colorful_paintSplatter', mood: 'creative', colors: ['#FAFAE8', '#FF3366', '#3388FF', '#FFCC00'], tags: ['art', 'messy', 'expressive'], description: 'Colorful paint splatters on a light canvas', compatibleMusicMoods: ['playful', 'energetic', 'electronic'], recommendedFor: ['creative', 'music', 'action'] },
colors: ['#FAFAE8', '#FF3366', '#3388FF', '#FFCC00'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Canvas texture background
Draw.fillGradient(ctx, w, h, [c[0], H.darken(c[0], 0.05)], 135);
// Subtle canvas weave
var rng = H.seededRandom(200);
ctx.strokeStyle = H.rgba('#CCCCAA', 0.06);
ctx.lineWidth = 0.5;
for (var y = 0; y < h; y += 6) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
for (var x = 0; x < w; x += 6) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(201);
var splatColors = ['#FF3366', '#3388FF', '#FFCC00', '#44CC55', '#FF6622', '#AA44DD', '#FF88AA', '#22DDCC'];
// Large splatters
for (var i = 0; i < 7; i++) {
var sx = rng() * w;
var sy = rng() * h;
var sr = 20 + rng() * 45;
var col = H.rgba(splatColors[Math.floor(rng() * splatColors.length)], 0.5 + rng() * 0.3);
drawSplatter(ctx, sx, sy, sr, col, rng);
}
// Paint drips from top
for (var i = 0; i < 5; i++) {
var dx = rng() * w;
var dripLen = h * 0.2 + rng() * h * 0.5;
var dripColor = splatColors[Math.floor(rng() * splatColors.length)];
var dripW = 4 + rng() * 8;
// Animate drip length
var animLen = dripLen * H.clamp((Math.sin(t * 0.3 + i * 1.2) + 1) / 2, 0.6, 1);
ctx.fillStyle = H.rgba(dripColor, 0.4);
ctx.beginPath();
ctx.moveTo(dx - dripW / 2, 0);
ctx.lineTo(dx + dripW / 2, 0);
ctx.lineTo(dx + dripW * 0.3, animLen);
ctx.quadraticCurveTo(dx, animLen + 8, dx - dripW * 0.3, animLen);
ctx.closePath();
ctx.fill();
}
// Small accent dots
for (var i = 0; i < 20; i++) {
var px = rng() * w;
var py = rng() * h;
var pr = 2 + rng() * 5;
D.circle(ctx, px, py, pr, H.rgba(splatColors[Math.floor(rng() * splatColors.length)], 0.3 + rng() * 0.4));
}
},
particles: { type: 'particles', count: 20, color: '#FF3366' }
},
candyWorld: {
meta: { id: 'colorful_candyWorld', mood: 'sweet', colors: ['#FFE0F0', '#FF88BB', '#88DDFF', '#AAFFAA'], tags: ['candy', 'pastel', 'fun'], description: 'Candy landscape with lollipops, gumdrops, and sugar hills', compatibleMusicMoods: ['playful', 'dreamy', 'calm'], recommendedFor: ['pet', 'creative', 'puzzle'] },
colors: ['#FFE0F0', '#FF88BB', '#88DDFF', '#AAFFAA'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[2], c[0], c[3]], 90);
// Cotton candy clouds
Draw.clouds(ctx, w, h, h * 0.15, 4, '#FFAACC', 301);
Draw.clouds(ctx, w, h, h * 0.3, 3, '#AADDFF', 302);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(303);
// Candy hills (rounded, pastel)
Draw.hills(ctx, w, h, h * 0.72, 5, '#FFAADD', 304);
Draw.hills(ctx, w, h, h * 0.78, 4, '#AAFFCC', 305);
Draw.hills(ctx, w, h, h * 0.84, 5, '#FFCCEE', 306);
// Lollipops
var lolliColors = [['#FF4488', '#FFFFFF', '#FF88BB'], ['#44BBFF', '#FFFFFF', '#88DDFF'], ['#FFAA33', '#FFFFFF', '#FFDD88']];
for (var i = 0; i < 5; i++) {
var lx = rng() * w * 0.9 + w * 0.05;
var lBaseY = h * 0.72 + rng() * h * 0.12;
var lr = 10 + rng() * 14;
drawLollipop(ctx, lx, lBaseY, lr, lolliColors[Math.floor(rng() * lolliColors.length)], rng);
}
// Gumdrops
for (var i = 0; i < 8; i++) {
var gx = rng() * w;
var gy = h * 0.78 + rng() * h * 0.1;
var gr = 8 + rng() * 12;
var gc = H.pick(['#FF6688', '#66CCFF', '#FFCC44', '#88EE88', '#DD88FF']);
ctx.fillStyle = gc;
ctx.beginPath();
ctx.arc(gx, gy - gr * 0.5, gr, Math.PI, 0);
ctx.lineTo(gx + gr, gy);
ctx.lineTo(gx - gr, gy);
ctx.closePath();
ctx.fill();
// Sugar sparkle
ctx.fillStyle = H.rgba('#FFFFFF', 0.4);
ctx.beginPath();
ctx.arc(gx - gr * 0.3, gy - gr * 0.6, gr * 0.2, 0, Math.PI * 2);
ctx.fill();
}
// Candy cane stripes in ground
var stripeOffset = (t * 10) % 20;
ctx.save();
ctx.globalAlpha = 0.08;
for (var i = -1; i < w / 20 + 1; i++) {
Draw.rect(ctx, i * 20 + stripeOffset, h * 0.84, 10, h * 0.16, '#FF88AA');
}
ctx.restore();
// Floating candy
for (var i = 0; i < 4; i++) {
var fx = rng() * w;
var fy = h * 0.1 + rng() * h * 0.35 + Math.sin(t * 0.8 + i * 1.5) * 10;
var fr = 4 + rng() * 6;
D.circle(ctx, fx, fy, fr, H.rgba(H.pick(['#FF88AA', '#88CCFF', '#FFDD66', '#AAFFAA']), 0.5));
D.glow(ctx, fx, fy, fr * 2, H.rgba('#FFFFFF', 0.1));
}
},
particles: { type: 'sparkles', count: 30, color: '#FFAACC' }
},
disco: {
meta: { id: 'colorful_disco', mood: 'energetic', colors: ['#1A0A2E', '#2A1A44', '#FF33AA', '#33FFCC'], tags: ['dance', 'retro', 'lights'], description: 'Disco floor with mirror ball and sweeping colored lights', compatibleMusicMoods: ['energetic', 'electronic', 'playful'], recommendedFor: ['music', 'action', 'sports'] },
colors: ['#1A0A2E', '#2A1A44', '#FF33AA', '#33FFCC'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[0]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(400);
// Disco floor (checkered at bottom, perspective)
var floorY = h * 0.7;
for (var row = 0; row < 6; row++) {
for (var col = 0; col < 10; col++) {
var tileW = w / 8;
var tileH = (h - floorY) / 6;
var tx = col * tileW - tileW;
var ty = floorY + row * tileH;
var isLight = (row + col) % 2 === 0;
var pulse = Math.sin(t * 3 + row + col * 0.7);
var tileColor;
if (isLight && pulse > 0) {
var hueShift = (t * 0.5 + row * 0.3 + col * 0.2) % 1;
var idx = Math.floor(hueShift * RAINBOW.length);
tileColor = H.rgba(RAINBOW[idx], 0.3 + pulse * 0.2);
} else {
tileColor = isLight ? H.rgba(c[1], 0.6) : H.rgba(c[0], 0.8);
}
Draw.rect(ctx, tx, ty, tileW, tileH, tileColor);
}
}
// Disco ball
drawDiscoBall(ctx, w * 0.5, h * 0.18, 30, t);
// String to ceiling
ctx.strokeStyle = '#888888';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(w * 0.5, 0);
ctx.lineTo(w * 0.5, h * 0.18 - 30);
ctx.stroke();
// Sweeping light beams from disco ball
for (var i = 0; i < 6; i++) {
var beamAngle = t * 0.8 + i * Math.PI / 3;
var beamX = w * 0.5 + Math.cos(beamAngle) * w * 0.6;
var beamY = h;
var beamColor = RAINBOW[i % RAINBOW.length];
ctx.save();
ctx.globalAlpha = 0.06;
ctx.fillStyle = beamColor;
ctx.beginPath();
ctx.moveTo(w * 0.5, h * 0.18);
ctx.lineTo(beamX - 30, beamY);
ctx.lineTo(beamX + 30, beamY);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Reflected light spots on walls
for (var i = 0; i < 12; i++) {
var rx = rng() * w;
var ry = rng() * h * 0.65;
var rr = 3 + rng() * 8;
var spotBright = 0.1 + Math.sin(t * 2.5 + i * 1.3) * 0.1;
D.circle(ctx, rx, ry, rr, H.rgba(RAINBOW[Math.floor(rng() * RAINBOW.length)], spotBright));
}
},
particles: { type: 'sparkles', count: 40, color: '#FFFFFF' }
},
neon: {
meta: { id: 'colorful_neon', mood: 'electric', colors: ['#0A0A18', '#151530', '#FF0088', '#00FFCC'], tags: ['neon', 'glow', 'night'], description: 'Neon signs and glowing outlines against dark city night', compatibleMusicMoods: ['electronic', 'energetic', 'tense'], recommendedFor: ['action', 'racing', 'music'] },
colors: ['#0A0A18', '#151530', '#FF0088', '#00FFCC'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(500);
var neonColors = ['#FF0088', '#00FFCC', '#FFFF00', '#FF4400', '#8844FF', '#00AAFF'];
// Neon grid on floor
var gridY = h * 0.75;
var gridColor = H.rgba(c[3], 0.1 + Math.sin(t * 0.5) * 0.03);
for (var x = 0; x <= w; x += 40) {
ctx.strokeStyle = gridColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, gridY);
ctx.lineTo(x, h);
ctx.stroke();
}
for (var y = gridY; y <= h; y += 20) {
ctx.strokeStyle = gridColor;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
// Neon sign shapes
var neonShapes = [
{ type: 'circle', x: w * 0.2, y: h * 0.3, r: 35 },
{ type: 'triangle', x: w * 0.5, y: h * 0.25, r: 40 },
{ type: 'rect', x: w * 0.78, y: h * 0.3, r: 30 }
];
for (var i = 0; i < neonShapes.length; i++) {
var s = neonShapes[i];
var nc = neonColors[i % neonColors.length];
var flicker = 0.6 + Math.sin(t * 3 + i * 2.1) * 0.2;
// Outer glow
D.glow(ctx, s.x, s.y, s.r * 2, H.rgba(nc, flicker * 0.15));
ctx.strokeStyle = H.rgba(nc, flicker);
ctx.lineWidth = 3;
ctx.shadowColor = nc;
ctx.shadowBlur = 15;
if (s.type === 'circle') {
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.stroke();
} else if (s.type === 'triangle') {
ctx.beginPath();
for (var v = 0; v < 3; v++) {
var a = v * Math.PI * 2 / 3 - Math.PI / 2;
var px = s.x + Math.cos(a) * s.r;
var py = s.y + Math.sin(a) * s.r;
v === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.closePath();
ctx.stroke();
} else {
ctx.strokeRect(s.x - s.r, s.y - s.r * 0.7, s.r * 2, s.r * 1.4);
}
ctx.shadowBlur = 0;
}
// Neon lines / tubes
for (var i = 0; i < 5; i++) {
var lx1 = rng() * w;
var ly1 = rng() * h * 0.6;
var lx2 = lx1 + (rng() - 0.5) * 100;
var ly2 = ly1 + (rng() - 0.5) * 60;
var lc = neonColors[Math.floor(rng() * neonColors.length)];
var lFlicker = 0.3 + Math.sin(t * 4 + rng() * 10) * 0.2;
D.glow(ctx, (lx1 + lx2) / 2, (ly1 + ly2) / 2, 20, H.rgba(lc, lFlicker * 0.1));
ctx.strokeStyle = H.rgba(lc, lFlicker);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(lx1, ly1);
ctx.lineTo(lx2, ly2);
ctx.stroke();
}
// Cityscape silhouettes
Draw.buildings(ctx, w, h, h * 0.75, 10, [c[0], c[1]], 501);
},
particles: { type: 'particles', count: 30, color: '#FF0088' }
},
tieDye: {
meta: { id: 'colorful_tieDye', mood: 'psychedelic', colors: ['#FF4488', '#FFAA22', '#44DDBB', '#8844FF'], tags: ['swirl', 'organic', 'hippie'], description: 'Swirling tie-dye patterns with organic color blending', compatibleMusicMoods: ['dreamy', 'ambient', 'playful'], recommendedFor: ['creative', 'music', 'story'] },
colors: ['#FF4488', '#FFAA22', '#44DDBB', '#8844FF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2], c[3]], 90 + t * 3);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(600);
// Concentric swirl rings from multiple centers
var centers = [];
for (var i = 0; i < 4; i++) {
centers.push({ x: rng() * w, y: rng() * h });
}
for (var ci = 0; ci < centers.length; ci++) {
var center = centers[ci];
var maxR = 80 + rng() * 120;
for (var ring = 8; ring >= 0; ring--) {
var rr = maxR * (ring / 8);
var colorIdx = (ring + ci) % c.length;
var alpha = 0.12 + ring * 0.01;
var spiral = t * 0.2 * (ci % 2 === 0 ? 1 : -1);
ctx.fillStyle = H.rgba(c[colorIdx], alpha);
ctx.beginPath();
// Organic wavy circle
for (var a = 0; a <= 36; a++) {
var angle = (a / 36) * Math.PI * 2 + spiral;
var wobble = 1 + Math.sin(angle * 3 + ring + t * 0.5) * 0.15;
var px = center.x + Math.cos(angle) * rr * wobble;
var py = center.y + Math.sin(angle) * rr * wobble;
a === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
}
}
// Color bleed spots
for (var i = 0; i < 10; i++) {
var bx = rng() * w, by = rng() * h;
var br = 20 + rng() * 50;
var bc = c[Math.floor(rng() * c.length)];
D.glow(ctx, bx, by, br, H.rgba(bc, 0.08 + Math.sin(t * 0.7 + i) * 0.03));
}
// Radial lines from center
var cx = w * 0.5, cy = h * 0.5;
ctx.lineWidth = 1;
for (var i = 0; i < 16; i++) {
var la = (i / 16) * Math.PI * 2 + t * 0.1;
ctx.strokeStyle = H.rgba(c[i % c.length], 0.05);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(la) * w, cy + Math.sin(la) * h);
ctx.stroke();
}
},
particles: { type: 'particles', count: 25, color: '#FF88CC' }
},
gradient: {
meta: { id: 'colorful_gradient', mood: 'peaceful', colors: ['#FF6B6B', '#FECA57', '#48DBFB', '#FF9FF3'], tags: ['smooth', 'modern', 'clean'], description: 'Smooth shifting color gradients with geometric accents', compatibleMusicMoods: ['ambient', 'calm', 'dreamy'], recommendedFor: ['puzzle', 'creative', 'trivia'] },
colors: ['#FF6B6B', '#FECA57', '#48DBFB', '#FF9FF3'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Animated gradient angle
var angle = 90 + Math.sin(t * 0.2) * 30;
// Cycle through color pairs
var shift = (Math.sin(t * 0.15) + 1) / 2;
var c1 = H.blendColor(c[0], c[2], shift);
var c2 = H.blendColor(c[1], c[3], shift);
var c3 = H.blendColor(c[2], c[0], shift);
Draw.fillGradient(ctx, w, h, [c1, c2, c3], angle);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(700);
// Soft geometric shapes floating
for (var i = 0; i < 8; i++) {
var sx = rng() * w;
var sy = rng() * h;
var sr = 20 + rng() * 60;
var sides = 3 + Math.floor(rng() * 4);
var rot = t * 0.1 * (rng() > 0.5 ? 1 : -1);
var col = c[Math.floor(rng() * c.length)];
var alpha = 0.06 + Math.sin(t * 0.5 + i) * 0.03;
D.polygon(ctx, sx, sy, sr, sides, H.rgba(col, alpha), rot);
// Soft outline
ctx.strokeStyle = H.rgba(col, alpha * 1.5);
ctx.lineWidth = 1.5;
ctx.beginPath();
for (var v = 0; v <= sides; v++) {
var va = (v / sides) * Math.PI * 2 + rot;
var vx = sx + Math.cos(va) * sr;
var vy = sy + Math.sin(va) * sr;
v === 0 ? ctx.moveTo(vx, vy) : ctx.lineTo(vx, vy);
}
ctx.stroke();
}
// Floating circles
for (var i = 0; i < 12; i++) {
var cx = rng() * w;
var cy = rng() * h;
var cr = 5 + rng() * 25;
var bob = Math.sin(t * 0.4 + i * 0.8) * 8;
ctx.strokeStyle = H.rgba(c[Math.floor(rng() * c.length)], 0.1 + rng() * 0.08);
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy + bob, cr, 0, Math.PI * 2);
ctx.stroke();
}
// Soft horizontal bands
for (var i = 0; i < 4; i++) {
var bY = h * 0.15 + i * h * 0.22;
var bAlpha = 0.03 + Math.sin(t * 0.3 + i * 1.5) * 0.015;
ctx.fillStyle = H.rgba(c[i % c.length], bAlpha);
ctx.fillRect(0, bY, w, h * 0.08);
}
},
particles: { type: 'sparkles', count: 20, color: '#FFFFFF' }
},
prism: {
meta: { id: 'colorful_prism', mood: 'mystical', colors: ['#1A1A2E', '#0F3460', '#E94560', '#FFFFFF'], tags: ['light', 'crystal', 'spectrum'], description: 'Light splitting through prisms into rainbow beams', compatibleMusicMoods: ['epic', 'ambient', 'electronic'], recommendedFor: ['puzzle', 'physics', 'creative'] },
colors: ['#1A1A2E', '#0F3460', '#E94560', '#FFFFFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[0]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(800);
var prismX = w * 0.45, prismY = h * 0.42;
var prismR = Math.min(w, h) * 0.12;
// Incoming white light beam
var beamPulse = 0.5 + Math.sin(t * 0.8) * 0.15;
ctx.save();
ctx.globalAlpha = beamPulse;
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 4;
ctx.shadowColor = '#FFFFFF';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.moveTo(0, prismY);
ctx.lineTo(prismX - prismR * 0.8, prismY);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.restore();
// Prism (triangle)
var g = D.linearGradient(ctx, prismX - prismR, prismY + prismR, prismX + prismR, prismY - prismR, [[0, H.rgba('#FFFFFF', 0.1)], [0.5, H.rgba('#AABBFF', 0.15)], [1, H.rgba('#FFFFFF', 0.05)]]);
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(prismX, prismY - prismR);
ctx.lineTo(prismX + prismR * 0.87, prismY + prismR * 0.5);
ctx.lineTo(prismX - prismR * 0.87, prismY + prismR * 0.5);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = H.rgba('#FFFFFF', 0.3);
ctx.lineWidth = 1.5;
ctx.stroke();
// Rainbow beams emanating from prism
for (var i = 0; i < RAINBOW.length; i++) {
var angle = 0.15 + (i / RAINBOW.length) * 0.7;
var endX = w;
var endY = prismY - h * 0.3 + i * (h * 0.12);
var beamAlpha = 0.2 + Math.sin(t * 1.2 + i * 0.5) * 0.08;
ctx.strokeStyle = H.rgba(RAINBOW[i], beamAlpha);
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(prismX + prismR * 0.5, prismY);
ctx.lineTo(endX, endY);
ctx.stroke();
// Glow along beam
D.glow(ctx, (prismX + endX) / 2, (prismY + endY) / 2, 30, H.rgba(RAINBOW[i], beamAlpha * 0.15));
}
// Secondary smaller prisms
for (var i = 0; i < 3; i++) {
var spx = rng() * w * 0.3 + w * 0.6;
var spy = rng() * h * 0.7 + h * 0.15;
var spr = 10 + rng() * 15;
var rot = rng() * Math.PI + t * 0.1;
ctx.strokeStyle = H.rgba('#FFFFFF', 0.1);
ctx.lineWidth = 1;
ctx.beginPath();
for (var v = 0; v < 3; v++) {
var va = rot + v * Math.PI * 2 / 3;
var vx = spx + Math.cos(va) * spr;
var vy = spy + Math.sin(va) * spr;
v === 0 ? ctx.moveTo(vx, vy) : ctx.lineTo(vx, vy);
}
ctx.closePath();
ctx.stroke();
// Tiny rainbow from small prism
for (var j = 0; j < 3; j++) {
var microAngle = rot + 0.5 + j * 0.2;
ctx.strokeStyle = H.rgba(RAINBOW[j * 2], 0.08);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(spx, spy);
ctx.lineTo(spx + Math.cos(microAngle) * 50, spy + Math.sin(microAngle) * 50);
ctx.stroke();
}
}
// Light sparkles where beams hit edges
for (var i = 0; i < 8; i++) {
var sx = w * 0.8 + rng() * w * 0.2;
var sy = rng() * h;
var sparkle = 0.2 + Math.sin(t * 2 + rng() * 10) * 0.2;
D.circle(ctx, sx, sy, 2, H.rgba(RAINBOW[Math.floor(rng() * RAINBOW.length)], sparkle));
}
// Subtle scanlines for atmosphere
Draw.scanlines(ctx, w, h, 4, H.rgba(c[0], 0.04));
},
particles: { type: 'sparkles', count: 35, color: '#FFFFFF' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,505 @@
// fantasy.js — Fantasy theme backgrounds
// Variants: castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('fantasy', {
castle: {
meta: { id: 'fantasy_castle', mood: 'heroic', colors: ['#1a1040', '#2d1b69', '#8b7355', '#ffd700'], tags: ['castle', 'medieval', 'heroic', 'stone'], description: 'A grand castle silhouette against a twilight sky with golden banners', compatibleMusicMoods: ['epic', 'heroic', 'adventure'], recommendedFor: ['rpg', 'action', 'story'] },
colors: ['#1a1040', '#2d1b69', '#8b7355', '#ffd700'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.lighten(c[1], 0.15)], 90);
Draw.stars(ctx, w, h * 0.5, 80, '#FFFFFF', 101, [0.5, 2]);
var moonGlow = 0.35 + Math.sin(t * 0.3) * 0.1;
Draw.glow(ctx, w * 0.8, h * 0.15, 80, H.rgba('#FFFFCC', moonGlow));
Draw.circle(ctx, w * 0.8, h * 0.15, 25, '#FFFFDD');
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(200);
Draw.mountains(ctx, w, h, h * 0.6, 6, H.darken(c[2], 0.6), 201);
// Castle wall
Draw.rect(ctx, w * 0.25, h * 0.4, w * 0.5, h * 0.6, H.darken(c[2], 0.4));
// Towers
var towerW = w * 0.08;
Draw.rect(ctx, w * 0.22, h * 0.25, towerW, h * 0.75, H.darken(c[2], 0.45));
Draw.rect(ctx, w * 0.70, h * 0.25, towerW, h * 0.75, H.darken(c[2], 0.45));
// Battlements
for (var i = 0; i < 12; i++) {
var bx = w * 0.25 + i * (w * 0.5 / 12);
if (i % 2 === 0) Draw.rect(ctx, bx, h * 0.38, w * 0.5 / 12, h * 0.04, H.darken(c[2], 0.4));
}
// Tower caps (triangles)
ctx.fillStyle = c[2];
ctx.beginPath(); ctx.moveTo(w * 0.22, h * 0.25); ctx.lineTo(w * 0.26, h * 0.15); ctx.lineTo(w * 0.30, h * 0.25); ctx.fill();
ctx.beginPath(); ctx.moveTo(w * 0.70, h * 0.25); ctx.lineTo(w * 0.74, h * 0.15); ctx.lineTo(w * 0.78, h * 0.25); ctx.fill();
// Gate
Draw.rect(ctx, w * 0.44, h * 0.6, w * 0.12, h * 0.4, '#0a0a15');
ctx.strokeStyle = c[3]; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(w * 0.5, h * 0.6, w * 0.06, Math.PI, 0); ctx.stroke();
// Windows with flickering glow
for (var j = 0; j < 6; j++) {
var wx = w * 0.3 + rng() * w * 0.4;
var wy = h * 0.42 + rng() * h * 0.15;
var flicker = 0.4 + Math.sin(t * 2 + j * 1.7) * 0.2;
Draw.glow(ctx, wx, wy, 12, H.rgba(c[3], flicker));
Draw.rect(ctx, wx - 3, wy - 5, 6, 10, H.rgba(c[3], flicker + 0.2));
}
// Banner
var bannerSway = Math.sin(t * 1.5) * 3;
ctx.fillStyle = '#8B0000';
ctx.beginPath(); ctx.moveTo(w * 0.26, h * 0.16); ctx.lineTo(w * 0.26 + bannerSway, h * 0.22); ctx.lineTo(w * 0.26 - 8, h * 0.22); ctx.fill();
},
particles: { type: 'fireflies', count: 12, color: '#FFD700' }
},
dungeon: {
meta: { id: 'fantasy_dungeon', mood: 'dark', colors: ['#0a0a0f', '#1a1520', '#4a3728', '#ff6a00'], tags: ['dungeon', 'dark', 'underground', 'torches'], description: 'Dark stone corridors lit by flickering torches and glowing embers', compatibleMusicMoods: ['dark', 'tense', 'mysterious'], recommendedFor: ['rpg', 'action', 'puzzle'] },
colors: ['#0a0a0f', '#1a1520', '#4a3728', '#ff6a00'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Stone texture
var rng = H.seededRandom(300);
for (var i = 0; i < 40; i++) {
var sx = rng() * w, sy = rng() * h;
var sw = 20 + rng() * 60, sh = 15 + rng() * 40;
Draw.rect(ctx, sx, sy, sw, sh, H.rgba(c[2], 0.08 + rng() * 0.06));
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Stone pillars
for (var p = 0; p < 3; p++) {
var px = w * 0.2 + p * w * 0.3;
Draw.rect(ctx, px - 15, h * 0.1, 30, h * 0.9, H.darken(c[2], 0.3));
Draw.rect(ctx, px - 20, h * 0.08, 40, 15, H.darken(c[2], 0.2));
Draw.rect(ctx, px - 20, h * 0.92, 40, 15, H.darken(c[2], 0.2));
}
// Chains hanging
var rng = H.seededRandom(301);
for (var ch = 0; ch < 4; ch++) {
var cx = w * 0.15 + rng() * w * 0.7;
var chainLen = h * 0.1 + rng() * h * 0.2;
ctx.strokeStyle = H.rgba('#666655', 0.5);
ctx.lineWidth = 2;
for (var link = 0; link < chainLen; link += 8) {
ctx.beginPath();
ctx.ellipse(cx, link + h * 0.02, 3, 4, 0, 0, Math.PI * 2);
ctx.stroke();
}
}
// Torches with animated glow
for (var ti = 0; ti < 4; ti++) {
var tx = w * 0.1 + ti * w * 0.27;
var ty = h * 0.35;
Draw.rect(ctx, tx - 3, ty, 6, 20, '#5a3a1a');
var flicker = 0.5 + Math.sin(t * 4 + ti * 2.3) * 0.3 + Math.sin(t * 7 + ti) * 0.15;
Draw.glow(ctx, tx, ty - 5, 40 + Math.sin(t * 3 + ti) * 8, H.rgba(c[3], flicker * 0.4));
Draw.circle(ctx, tx, ty - 5, 5, H.rgba(c[3], flicker));
Draw.circle(ctx, tx, ty - 8, 3, H.rgba('#FFCC44', flicker));
}
// Ground cracks
ctx.strokeStyle = H.rgba('#222', 0.3);
ctx.lineWidth = 1;
var crng = H.seededRandom(302);
for (var cr = 0; cr < 8; cr++) {
var crx = crng() * w;
ctx.beginPath(); ctx.moveTo(crx, h * 0.9);
for (var seg = 0; seg < 4; seg++) {
crx += (crng() - 0.5) * 30;
ctx.lineTo(crx, h * 0.9 + seg * 8 + crng() * 10);
}
ctx.stroke();
}
},
particles: { type: 'particles', count: 20, color: '#FF6A00' }
},
enchantedForest: {
meta: { id: 'fantasy_enchantedForest', mood: 'mystical', colors: ['#0a1a0f', '#0d2818', '#1a4a2a', '#44ff88'], tags: ['forest', 'magical', 'nature', 'enchanted'], description: 'A mystical forest with glowing mushrooms and magical light filtering through ancient trees', compatibleMusicMoods: ['mystical', 'calm', 'magical'], recommendedFor: ['rpg', 'story', 'puzzle'] },
colors: ['#0a1a0f', '#0d2818', '#1a4a2a', '#44ff88'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.darken(c[2], 0.3)], 90);
// Filtered light rays
for (var r = 0; r < 5; r++) {
var rx = w * 0.15 + r * w * 0.18;
var rayAlpha = 0.03 + Math.sin(t * 0.5 + r) * 0.015;
ctx.fillStyle = H.rgba(c[3], rayAlpha);
ctx.beginPath();
ctx.moveTo(rx - 5, 0); ctx.lineTo(rx + 30, h);
ctx.lineTo(rx - 30, h); ctx.lineTo(rx + 5, 0);
ctx.fill();
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Background trees
Draw.trees(ctx, w, h, h * 0.85, 8, '#1a0d0a', H.darken(c[2], 0.3), 401);
// Large foreground trees
var rng = H.seededRandom(402);
for (var tr = 0; tr < 4; tr++) {
var tx = rng() * w;
var treeH = h * 0.5 + rng() * h * 0.3;
var trunkW = 12 + rng() * 10;
Draw.rect(ctx, tx - trunkW / 2, h - treeH, trunkW, treeH, '#2a1a0f');
// Roots
for (var rt = 0; rt < 3; rt++) {
ctx.fillStyle = '#2a1a0f';
ctx.beginPath();
ctx.moveTo(tx - trunkW / 2 - rt * 8, h);
ctx.quadraticCurveTo(tx - trunkW / 2, h - 15, tx - trunkW / 4, h - treeH * 0.1);
ctx.lineTo(tx - trunkW / 2, h - treeH * 0.1);
ctx.fill();
}
// Canopy
for (var can = 0; can < 3; can++) {
var canR = 30 + rng() * 40;
var canX = tx + (rng() - 0.5) * 30;
var canY = h - treeH - canR * 0.3 + can * 15;
Draw.circle(ctx, canX, canY, canR, H.rgba(c[2], 0.7 + rng() * 0.3));
}
}
// Glowing mushrooms
for (var m = 0; m < 8; m++) {
var mx = rng() * w, my = h * 0.82 + rng() * h * 0.15;
var mSize = 4 + rng() * 8;
var pulse = 0.4 + Math.sin(t * 1.2 + m * 1.1) * 0.2;
Draw.glow(ctx, mx, my, mSize * 3, H.rgba(c[3], pulse * 0.3));
Draw.rect(ctx, mx - 1, my, 2, mSize, '#8B7355');
ctx.fillStyle = H.rgba(c[3], pulse + 0.3);
ctx.beginPath(); ctx.ellipse(mx, my, mSize, mSize * 0.5, 0, Math.PI, 0); ctx.fill();
}
// Ground moss/grass
Draw.hills(ctx, w, h, h * 0.88, 10, H.darken(c[2], 0.2), 403);
},
particles: { type: 'fireflies', count: 25, color: '#44FF88' }
},
floatingIslands: {
meta: { id: 'fantasy_floatingIslands', mood: 'wonder', colors: ['#1a0a3a', '#2d1b69', '#5a8a5a', '#88ccff'], tags: ['sky', 'floating', 'islands', 'clouds'], description: 'Majestic islands suspended in a violet sky with waterfalls cascading into the void', compatibleMusicMoods: ['wonder', 'adventure', 'calm'], recommendedFor: ['rpg', 'puzzle', 'creative'] },
colors: ['#1a0a3a', '#2d1b69', '#5a8a5a', '#88ccff'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [H.darken(c[0], 0.3), c[0], c[1], H.lighten(c[1], 0.1)], 90);
Draw.stars(ctx, w, h * 0.4, 60, '#FFFFFF', 501, [0.5, 1.5]);
Draw.clouds(ctx, w, h, h * 0.2, 5, c[3], 502);
Draw.clouds(ctx, w, h, h * 0.7, 4, H.rgba(c[3], 0.5), 503);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(504);
// Draw 3 floating islands
var islands = [
{ x: w * 0.2, y: h * 0.35, sz: 1.0 },
{ x: w * 0.65, y: h * 0.25, sz: 0.7 },
{ x: w * 0.45, y: h * 0.55, sz: 0.5 }
];
for (var i = 0; i < islands.length; i++) {
var isl = islands[i];
var bob = Math.sin(t * 0.4 + i * 2) * 5;
var ix = isl.x, iy = isl.y + bob, sz = isl.sz;
// Bottom rocky underside
ctx.fillStyle = H.darken(c[2], 0.5);
ctx.beginPath();
ctx.moveTo(ix - 60 * sz, iy + 10); ctx.quadraticCurveTo(ix - 20 * sz, iy + 80 * sz, ix, iy + 100 * sz);
ctx.quadraticCurveTo(ix + 30 * sz, iy + 70 * sz, ix + 60 * sz, iy + 10);
ctx.fill();
// Top grassy surface
ctx.fillStyle = c[2];
ctx.beginPath(); ctx.ellipse(ix, iy, 65 * sz, 15 * sz, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = H.lighten(c[2], 0.15);
ctx.beginPath(); ctx.ellipse(ix, iy - 3, 60 * sz, 10 * sz, 0, 0, Math.PI * 2); ctx.fill();
// Small trees on top
if (sz > 0.6) {
for (var tr = 0; tr < 3; tr++) {
var ttx = ix - 30 * sz + rng() * 60 * sz;
Draw.rect(ctx, ttx - 2, iy - 20 * sz, 4, 15 * sz, '#3a2a15');
Draw.circle(ctx, ttx, iy - 25 * sz, 8 * sz, H.lighten(c[2], 0.1));
}
}
// Waterfall from main island
if (i === 0) {
var wfAlpha = 0.2 + Math.sin(t * 2) * 0.08;
ctx.fillStyle = H.rgba(c[3], wfAlpha);
ctx.fillRect(ix + 20, iy + 10, 6, h - iy);
for (var sp = 0; sp < 5; sp++) {
var spy = iy + 10 + rng() * (h - iy);
Draw.glow(ctx, ix + 23, spy, 8, H.rgba(c[3], 0.15));
}
}
}
},
particles: { type: 'sparkles', count: 35, color: '#88CCFF' }
},
magicalLibrary: {
meta: { id: 'fantasy_magicalLibrary', mood: 'scholarly', colors: ['#1a0f0a', '#2d1a10', '#6b4423', '#cc88ff'], tags: ['library', 'books', 'magical', 'scholarly'], description: 'Towering bookshelves with floating magical tomes and arcane symbols', compatibleMusicMoods: ['calm', 'mystical', 'scholarly'], recommendedFor: ['puzzle', 'trivia', 'story'] },
colors: ['#1a0f0a', '#2d1a10', '#6b4423', '#cc88ff'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Warm ambient light
Draw.glow(ctx, w * 0.5, h * 0.3, w * 0.6, H.rgba('#553311', 0.15));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(600);
// Bookshelves
var shelfColors = ['#8B4513', '#6B3410', '#5a2d0e', '#7a3d18'];
for (var s = 0; s < 5; s++) {
var sx = s * w * 0.2 + w * 0.02;
var sw = w * 0.18;
// Shelf frame
Draw.rect(ctx, sx, h * 0.05, sw, h * 0.92, H.darken(c[2], 0.4));
// Shelves and books
for (var row = 0; row < 6; row++) {
var sy = h * 0.1 + row * h * 0.14;
Draw.rect(ctx, sx, sy + h * 0.11, sw, 4, c[2]);
// Books on shelf
var bx = sx + 3;
while (bx < sx + sw - 5) {
var bw = 4 + rng() * 8;
var bh = h * 0.08 + rng() * h * 0.03;
var bookColor = H.pick(['#AA2222', '#2244AA', '#22AA44', '#8844AA', '#AA8822', '#228888']);
Draw.rect(ctx, bx, sy + h * 0.11 - bh, bw, bh, bookColor);
if (rng() > 0.6) {
Draw.rect(ctx, bx + 1, sy + h * 0.11 - bh + 2, bw - 2, 2, H.lighten(bookColor, 0.3));
}
bx += bw + 1;
}
}
}
// Floating books
for (var fb = 0; fb < 4; fb++) {
var fbx = w * 0.15 + rng() * w * 0.7;
var fby = h * 0.2 + rng() * h * 0.4 + Math.sin(t * 0.8 + fb * 1.5) * 10;
var rot = Math.sin(t * 0.3 + fb) * 0.2;
ctx.save();
ctx.translate(fbx, fby); ctx.rotate(rot);
Draw.rect(ctx, -10, -7, 20, 14, H.pick(['#882222', '#224488', '#228844']));
Draw.rect(ctx, -8, -5, 16, 1, '#FFD700');
ctx.restore();
Draw.glow(ctx, fbx, fby, 20, H.rgba(c[3], 0.2 + Math.sin(t + fb) * 0.1));
}
// Arcane symbols floating
var symbols = ['\u2606', '\u2609', '\u263E', '\u2726'];
ctx.font = '16px serif';
for (var sym = 0; sym < 6; sym++) {
var smx = rng() * w, smy = rng() * h;
var symAlpha = 0.15 + Math.sin(t * 0.7 + sym * 1.3) * 0.1;
ctx.fillStyle = H.rgba(c[3], symAlpha);
ctx.fillText(symbols[sym % symbols.length], smx + Math.sin(t * 0.4 + sym) * 5, smy);
}
},
particles: { type: 'sparkles', count: 20, color: '#CC88FF' }
},
crystalCave: {
meta: { id: 'fantasy_crystalCave', mood: 'ethereal', colors: ['#0a0a1a', '#0f1a2d', '#225588', '#44ddff'], tags: ['cave', 'crystals', 'underground', 'glow'], description: 'An underground cavern filled with luminous crystal formations and reflected light', compatibleMusicMoods: ['ethereal', 'mystical', 'calm'], recommendedFor: ['puzzle', 'rpg', 'creative'] },
colors: ['#0a0a1a', '#0f1a2d', '#225588', '#44ddff'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Cavern ceiling
var rng = H.seededRandom(700);
ctx.fillStyle = H.darken(c[1], 0.3);
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, h * 0.15);
for (var i = 0; i <= 20; i++) {
var cx = (i / 20) * w;
var cy = h * 0.08 + rng() * h * 0.12;
ctx.lineTo(cx, cy);
}
ctx.lineTo(w, 0); ctx.closePath(); ctx.fill();
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(701);
// Stalactites
for (var st = 0; st < 12; st++) {
var stx = rng() * w;
var stLen = h * 0.05 + rng() * h * 0.15;
var stW = 3 + rng() * 8;
ctx.fillStyle = H.darken(c[2], 0.4);
ctx.beginPath();
ctx.moveTo(stx - stW, h * 0.08 + rng() * h * 0.07);
ctx.lineTo(stx, h * 0.08 + stLen);
ctx.lineTo(stx + stW, h * 0.08 + rng() * h * 0.07);
ctx.fill();
}
// Crystal clusters
var crystalHues = [c[2], c[3], H.lighten(c[2], 0.2), H.lighten(c[3], 0.2)];
for (var cl = 0; cl < 8; cl++) {
var clx = rng() * w;
var cly = h * 0.6 + rng() * h * 0.35;
var clSize = 15 + rng() * 30;
var pulse = 0.4 + Math.sin(t * 0.8 + cl * 0.9) * 0.2;
Draw.glow(ctx, clx, cly, clSize * 2, H.rgba(crystalHues[cl % crystalHues.length], pulse * 0.25));
// Individual crystal shards
for (var sh = 0; sh < 3 + Math.floor(rng() * 3); sh++) {
var shAngle = -Math.PI / 2 + (rng() - 0.5) * 1.2;
var shLen = clSize * (0.5 + rng() * 0.8);
var shW = 3 + rng() * 5;
var shColor = H.rgba(crystalHues[Math.floor(rng() * crystalHues.length)], pulse + 0.2);
ctx.fillStyle = shColor;
ctx.save();
ctx.translate(clx + (rng() - 0.5) * 10, cly);
ctx.rotate(shAngle);
ctx.beginPath();
ctx.moveTo(-shW / 2, 0); ctx.lineTo(0, -shLen); ctx.lineTo(shW / 2, 0);
ctx.fill();
ctx.restore();
}
}
// Cave floor with stalagmites
Draw.hills(ctx, w, h, h * 0.9, 15, H.darken(c[1], 0.4), 702);
for (var sg = 0; sg < 6; sg++) {
var sgx = rng() * w, sgH = h * 0.05 + rng() * h * 0.1;
ctx.fillStyle = H.darken(c[2], 0.3);
ctx.beginPath();
ctx.moveTo(sgx - 5, h * 0.9); ctx.lineTo(sgx, h * 0.9 - sgH); ctx.lineTo(sgx + 5, h * 0.9);
ctx.fill();
}
// Water reflections on cave floor
var refAlpha = 0.06 + Math.sin(t * 0.5) * 0.03;
Draw.water(ctx, w, h, h * 0.92, H.rgba(c[3], refAlpha), 2, 703);
},
particles: { type: 'sparkles', count: 40, color: '#44DDFF' }
},
temple: {
meta: { id: 'fantasy_temple', mood: 'sacred', colors: ['#1a1025', '#2d1a3a', '#c9b77a', '#ffd700'], tags: ['temple', 'sacred', 'ancient', 'pillars'], description: 'An ancient temple with towering marble pillars and sacred golden light', compatibleMusicMoods: ['sacred', 'epic', 'mystical'], recommendedFor: ['rpg', 'story', 'trivia'] },
colors: ['#1a1025', '#2d1a3a', '#c9b77a', '#ffd700'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.lighten(c[1], 0.08)], 90);
// Sacred light from above
var beamAlpha = 0.05 + Math.sin(t * 0.3) * 0.02;
ctx.fillStyle = H.rgba(c[3], beamAlpha);
ctx.beginPath();
ctx.moveTo(w * 0.4, 0); ctx.lineTo(w * 0.3, h); ctx.lineTo(w * 0.7, h); ctx.lineTo(w * 0.6, 0);
ctx.fill();
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Floor tiles
var tileColor1 = H.darken(c[2], 0.5), tileColor2 = H.darken(c[2], 0.55);
for (var ty = 0; ty < 5; ty++) {
for (var tx = 0; tx < 10; tx++) {
var color = (tx + ty) % 2 === 0 ? tileColor1 : tileColor2;
Draw.rect(ctx, tx * w * 0.1, h * 0.85 + ty * h * 0.03, w * 0.1, h * 0.03, color);
}
}
// Pillars
var pillarColor = H.lighten(c[2], 0.1);
for (var p = 0; p < 6; p++) {
var px = w * 0.08 + p * w * 0.18;
var pw = 18, ph = h * 0.6;
// Base
Draw.rect(ctx, px - pw, h * 0.83, pw * 2, h * 0.04, pillarColor);
// Shaft
Draw.rect(ctx, px - pw * 0.6, h * 0.25, pw * 1.2, ph, pillarColor);
// Fluting effect
for (var f = 0; f < 3; f++) {
Draw.rect(ctx, px - pw * 0.5 + f * pw * 0.4, h * 0.25, 2, ph, H.darken(pillarColor, 0.15));
}
// Capital
Draw.rect(ctx, px - pw, h * 0.22, pw * 2, h * 0.04, pillarColor);
Draw.rect(ctx, px - pw * 1.2, h * 0.21, pw * 2.4, h * 0.02, pillarColor);
}
// Altar
Draw.rect(ctx, w * 0.4, h * 0.65, w * 0.2, h * 0.2, H.darken(c[2], 0.3));
Draw.rect(ctx, w * 0.38, h * 0.63, w * 0.24, h * 0.04, c[2]);
// Sacred flame on altar
var flameSize = 12 + Math.sin(t * 3) * 4;
Draw.glow(ctx, w * 0.5, h * 0.58, flameSize * 3, H.rgba(c[3], 0.3 + Math.sin(t * 2) * 0.1));
Draw.circle(ctx, w * 0.5, h * 0.58, flameSize, H.rgba(c[3], 0.7 + Math.sin(t * 4) * 0.2));
// Runes on floor
var rng = H.seededRandom(800);
ctx.strokeStyle = H.rgba(c[3], 0.1 + Math.sin(t * 0.5) * 0.05);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(w * 0.5, h * 0.9, w * 0.15, 0, Math.PI * 2);
ctx.stroke();
for (var rn = 0; rn < 8; rn++) {
var angle = (rn / 8) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(w * 0.5, h * 0.9);
ctx.lineTo(w * 0.5 + Math.cos(angle) * w * 0.15, h * 0.9 + Math.sin(angle) * w * 0.15);
ctx.stroke();
}
},
particles: { type: 'dust', count: 25, color: '#FFD700' }
},
tower: {
meta: { id: 'fantasy_tower', mood: 'arcane', colors: ['#0a0a20', '#151540', '#554488', '#aa66ff'], tags: ['tower', 'wizard', 'arcane', 'night'], description: 'A tall wizard tower under a starlit sky with arcane energy crackling at its peak', compatibleMusicMoods: ['mystical', 'dark', 'arcane'], recommendedFor: ['rpg', 'puzzle', 'trivia'] },
colors: ['#0a0a20', '#151540', '#554488', '#aa66ff'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.stars(ctx, w, h * 0.6, 100, '#FFFFFF', 901, [0.5, 2.5]);
// Nebula glow
Draw.glow(ctx, w * 0.3, h * 0.2, 150, H.rgba(c[2], 0.08));
Draw.glow(ctx, w * 0.7, h * 0.15, 100, H.rgba(c[3], 0.05));
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Rolling hills
Draw.hills(ctx, w, h, h * 0.8, 8, H.darken(c[2], 0.6), 902);
// Tower structure
var towerX = w * 0.5, towerW = w * 0.08;
var towerBot = h * 0.85, towerTop = h * 0.15;
// Main shaft
Draw.rect(ctx, towerX - towerW / 2, towerTop, towerW, towerBot - towerTop, H.darken(c[2], 0.4));
// Stone texture lines
for (var sl = 0; sl < 15; sl++) {
var sly = towerTop + sl * (towerBot - towerTop) / 15;
Draw.rect(ctx, towerX - towerW / 2, sly, towerW, 1, H.rgba('#000', 0.15));
}
// Wider base
ctx.fillStyle = H.darken(c[2], 0.45);
ctx.beginPath();
ctx.moveTo(towerX - towerW * 0.8, towerBot);
ctx.lineTo(towerX - towerW / 2, towerBot - h * 0.1);
ctx.lineTo(towerX + towerW / 2, towerBot - h * 0.1);
ctx.lineTo(towerX + towerW * 0.8, towerBot);
ctx.fill();
// Pointed roof
ctx.fillStyle = H.darken(c[2], 0.2);
ctx.beginPath();
ctx.moveTo(towerX - towerW * 0.7, towerTop);
ctx.lineTo(towerX, towerTop - h * 0.08);
ctx.lineTo(towerX + towerW * 0.7, towerTop);
ctx.fill();
// Windows with glow
var rng = H.seededRandom(903);
for (var wi = 0; wi < 5; wi++) {
var wy = towerTop + h * 0.05 + wi * (towerBot - towerTop - h * 0.1) / 5;
var wGlow = 0.4 + Math.sin(t * 1.2 + wi * 1.5) * 0.2;
Draw.glow(ctx, towerX, wy, 15, H.rgba(c[3], wGlow * 0.3));
Draw.rect(ctx, towerX - 4, wy - 6, 8, 12, H.rgba(c[3], wGlow));
ctx.strokeStyle = H.darken(c[2], 0.3);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(towerX, wy - 6, 4, Math.PI, 0); ctx.stroke();
}
// Arcane energy at top
var arcAngle = t * 1.5;
var arcPulse = 0.4 + Math.sin(t * 2) * 0.2;
Draw.glow(ctx, towerX, towerTop - h * 0.08, 30 + Math.sin(t) * 10, H.rgba(c[3], arcPulse * 0.5));
for (var arc = 0; arc < 5; arc++) {
var aa = arcAngle + (arc / 5) * Math.PI * 2;
var ar = 15 + Math.sin(t * 3 + arc) * 5;
var ax = towerX + Math.cos(aa) * ar;
var ay = towerTop - h * 0.08 + Math.sin(aa) * ar * 0.5;
Draw.circle(ctx, ax, ay, 2, H.rgba(c[3], arcPulse));
}
// Lightning bolt effect (intermittent)
if (Math.sin(t * 0.7) > 0.9) {
ctx.strokeStyle = H.rgba(c[3], 0.6);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(towerX, towerTop - h * 0.08);
var lx = towerX, ly = towerTop - h * 0.08;
for (var bolt = 0; bolt < 4; bolt++) {
lx += (rng() - 0.5) * 30;
ly -= 15 + rng() * 10;
ctx.lineTo(lx, ly);
}
ctx.stroke();
}
},
particles: { type: 'sparkles', count: 30, color: '#AA66FF' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,479 @@
// indoor.js — Indoor environment background themes
// Variants: laboratory, classroom, bedroom, kitchen, arcade, office, library, gym
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('indoor', {
laboratory: {
meta: { id: 'indoor_laboratory', mood: 'mysterious', colors: ['#1a1e2e', '#2a3040', '#0f6b5e', '#44eebb'], tags: ['science', 'lab', 'tech', 'glow'], description: 'Dark science lab with glowing equipment and bubbling vials', compatibleMusicMoods: ['mysterious', 'tense', 'ambient'], recommendedFor: ['puzzle', 'trivia', 'rpg'] },
colors: ['#1a1e2e', '#2a3040', '#0f6b5e', '#44eebb'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.rect(ctx, 0, h * 0.75, w, h * 0.25, H.darken(c[0], 0.3));
ctx.strokeStyle = H.rgba(c[2], 0.15);
ctx.lineWidth = 1;
for (var i = 0; i < w; i += 60) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, h); ctx.stroke(); }
for (var j = 0; j < h; j += 60) { ctx.beginPath(); ctx.moveTo(0, j); ctx.lineTo(w, j); ctx.stroke(); }
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(101);
// Lab benches
Draw.rect(ctx, w * 0.05, h * 0.72, w * 0.4, h * 0.04, '#3a3a4a');
Draw.rect(ctx, w * 0.55, h * 0.72, w * 0.4, h * 0.04, '#3a3a4a');
// Beakers and vials
for (var i = 0; i < 6; i++) {
var bx = w * 0.08 + i * w * 0.07;
var bh = 20 + rng() * 25;
var glow = 0.4 + Math.sin(t * (1.5 + rng()) + i) * 0.3;
Draw.rect(ctx, bx, h * 0.72 - bh, 12, bh, H.rgba(c[3], glow));
Draw.rect(ctx, bx - 2, h * 0.72 - bh, 16, 3, H.rgba('#FFFFFF', 0.2));
}
// Glowing tanks
for (var j = 0; j < 3; j++) {
var tx = w * 0.58 + j * w * 0.13;
var pulse = 0.15 + Math.sin(t * 0.8 + j * 2) * 0.1;
Draw.rect(ctx, tx, h * 0.45, 30, h * 0.27, H.rgba(c[2], pulse));
Draw.rect(ctx, tx - 2, h * 0.44, 34, 4, '#555566');
Draw.rect(ctx, tx - 2, h * 0.72, 34, 4, '#555566');
Draw.glow(ctx, tx + 15, h * 0.58, 35, c[3]);
}
// Ceiling pipes
for (var p = 0; p < 4; p++) {
var py = h * 0.05 + p * 18;
Draw.rect(ctx, 0, py, w, 6, H.rgba('#556677', 0.3));
}
// Blinking lights on wall
for (var k = 0; k < 8; k++) {
var lx = w * 0.1 + rng() * w * 0.8;
var ly = h * 0.1 + rng() * h * 0.3;
var on = Math.sin(t * (2 + rng() * 3) + k * 1.7) > 0.3;
Draw.circle(ctx, lx, ly, 3, on ? c[3] : H.rgba(c[2], 0.2));
if (on) Draw.glow(ctx, lx, ly, 12, c[3]);
}
},
particles: { type: 'bubbles', count: 15, color: '#44eebb' }
},
classroom: {
meta: { id: 'indoor_classroom', mood: 'cheerful', colors: ['#f5f0e0', '#d4c9a8', '#5b8c5a', '#8b5e3c'], tags: ['school', 'learning', 'education', 'cozy'], description: 'Bright classroom with chalkboard, desks, and warm sunlight', compatibleMusicMoods: ['cheerful', 'calm', 'upbeat'], recommendedFor: ['trivia', 'puzzle', 'story'] },
colors: ['#f5f0e0', '#d4c9a8', '#5b8c5a', '#8b5e3c'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], H.darken(c[0], 0.05)], 90);
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, c[1]);
// Baseboard
Draw.rect(ctx, 0, h * 0.76, w, h * 0.03, H.darken(c[3], 0.2));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(202);
// Chalkboard
Draw.rect(ctx, w * 0.15, h * 0.15, w * 0.7, h * 0.35, '#2a4a2a');
Draw.rect(ctx, w * 0.14, h * 0.14, w * 0.72, h * 0.37, 'transparent');
ctx.strokeStyle = c[3]; ctx.lineWidth = 4;
ctx.strokeRect(w * 0.14, h * 0.14, w * 0.72, h * 0.37);
// Chalk text squiggles
ctx.strokeStyle = H.rgba('#FFFFFF', 0.3); ctx.lineWidth = 1;
for (var i = 0; i < 5; i++) {
ctx.beginPath();
var ly = h * 0.22 + i * h * 0.06;
ctx.moveTo(w * 0.2, ly);
for (var x = w * 0.2; x < w * 0.75; x += 8) {
ctx.lineTo(x, ly + (rng() - 0.5) * 3);
}
ctx.stroke();
}
// Desks
for (var d = 0; d < 3; d++) {
var dy = h * 0.6 + d * h * 0.08;
for (var dx = 0; dx < 4; dx++) {
var xx = w * 0.12 + dx * w * 0.22;
Draw.rect(ctx, xx, dy, w * 0.16, h * 0.025, c[3]);
Draw.rect(ctx, xx + 5, dy + h * 0.025, 4, h * 0.04, H.darken(c[3], 0.3));
Draw.rect(ctx, xx + w * 0.14, dy + h * 0.025, 4, h * 0.04, H.darken(c[3], 0.3));
}
}
// Window with sunlight
Draw.rect(ctx, w * 0.02, h * 0.15, w * 0.08, h * 0.35, '#88bbdd');
ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 3;
ctx.strokeRect(w * 0.02, h * 0.15, w * 0.08, h * 0.35);
ctx.beginPath(); ctx.moveTo(w * 0.06, h * 0.15); ctx.lineTo(w * 0.06, h * 0.5); ctx.stroke();
var sunAlpha = 0.05 + Math.sin(t * 0.3) * 0.02;
ctx.fillStyle = H.rgba('#FFE488', sunAlpha);
ctx.beginPath(); ctx.moveTo(w * 0.1, h * 0.15); ctx.lineTo(w * 0.35, h * 0.78);
ctx.lineTo(w * 0.1, h * 0.78); ctx.closePath(); ctx.fill();
// Clock
var cx = w * 0.9, cy = h * 0.12;
Draw.circle(ctx, cx, cy, 18, '#FFFFFF');
ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(cx, cy, 18, 0, Math.PI * 2); ctx.stroke();
var angle = t * 0.1;
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(angle) * 12, cy + Math.sin(angle) * 12); ctx.stroke();
},
particles: { type: 'dust', count: 20, color: '#DDC888' }
},
bedroom: {
meta: { id: 'indoor_bedroom', mood: 'cozy', colors: ['#2a1f3d', '#3d2b5a', '#e8a87c', '#ffd166'], tags: ['cozy', 'night', 'home', 'warm'], description: 'Cozy bedroom at night with warm lamp glow and string lights', compatibleMusicMoods: ['calm', 'dreamy', 'ambient'], recommendedFor: ['story', 'creative', 'pet'] },
colors: ['#2a1f3d', '#3d2b5a', '#e8a87c', '#ffd166'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.rect(ctx, 0, h * 0.8, w, h * 0.2, H.darken(c[1], 0.3));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(303);
// Window with moonlight
Draw.rect(ctx, w * 0.6, h * 0.1, w * 0.2, h * 0.3, '#1a1a3e');
Draw.circle(ctx, w * 0.72, h * 0.18, 15, '#dde8ff');
Draw.glow(ctx, w * 0.72, h * 0.18, 40, '#aabbee');
ctx.strokeStyle = H.rgba('#FFFFFF', 0.3); ctx.lineWidth = 3;
ctx.strokeRect(w * 0.6, h * 0.1, w * 0.2, h * 0.3);
ctx.beginPath(); ctx.moveTo(w * 0.7, h * 0.1); ctx.lineTo(w * 0.7, h * 0.4); ctx.stroke();
// Bed
Draw.rect(ctx, w * 0.05, h * 0.55, w * 0.45, h * 0.25, '#6b4f7a');
Draw.rect(ctx, w * 0.05, h * 0.55, w * 0.12, h * 0.22, '#7a5f8a');
Draw.rect(ctx, w * 0.05, h * 0.5, w * 0.45, h * 0.06, H.darken('#6b4f7a', 0.3));
// Lamp with warm glow
var lampX = w * 0.88, lampY = h * 0.45;
Draw.rect(ctx, lampX - 3, lampY, 6, h * 0.35, '#666');
Draw.polygon(ctx, lampX, lampY - 5, 18, 3, c[2], -Math.PI / 2);
var lampGlow = 0.2 + Math.sin(t * 0.5) * 0.05;
Draw.glow(ctx, lampX, lampY - 10, 80, c[3]);
ctx.fillStyle = H.rgba(c[3], lampGlow);
ctx.beginPath(); ctx.moveTo(lampX - 40, lampY); ctx.lineTo(lampX + 40, lampY);
ctx.lineTo(lampX + 80, h * 0.8); ctx.lineTo(lampX - 80, h * 0.8); ctx.closePath(); ctx.fill();
// String lights across top
ctx.strokeStyle = H.rgba('#FFFFFF', 0.1); ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, h * 0.05);
for (var x = 0; x < w; x += 5) { ctx.lineTo(x, h * 0.05 + Math.sin(x * 0.02) * 10); }
ctx.stroke();
for (var i = 0; i < 12; i++) {
var lx = w * 0.05 + i * w * 0.08;
var ly = h * 0.05 + Math.sin(lx * 0.02) * 10;
var flicker = 0.5 + Math.sin(t * 2 + i * 1.3) * 0.3;
var lc = H.pick(['#ff6b6b', '#ffd166', '#06d6a0', '#118ab2', '#ef476f']);
Draw.circle(ctx, lx, ly + 5, 4, H.rgba(lc, flicker));
Draw.glow(ctx, lx, ly + 5, 12, lc);
}
// Bookshelf
Draw.rect(ctx, w * 0.55, h * 0.5, w * 0.15, h * 0.3, '#5a3e2b');
for (var s = 0; s < 3; s++) {
Draw.rect(ctx, w * 0.55, h * 0.5 + s * h * 0.1, w * 0.15, 3, '#4a2e1b');
for (var b = 0; b < 4; b++) {
var bw = 6 + rng() * 8;
var bc = H.pick(['#cc4444', '#4477bb', '#44aa66', '#ddaa33', '#8855aa']);
Draw.rect(ctx, w * 0.56 + b * (bw + 3), h * 0.5 + s * h * 0.1 + 4, bw, h * 0.09, bc);
}
}
},
particles: { type: 'fireflies', count: 8, color: '#ffd166' }
},
kitchen: {
meta: { id: 'indoor_kitchen', mood: 'warm', colors: ['#fff8e7', '#f0d9a0', '#c97b3a', '#8b4513'], tags: ['cooking', 'home', 'warm', 'food'], description: 'Bright kitchen with checkered floor, cabinets, and warm appliance glow', compatibleMusicMoods: ['cheerful', 'upbeat', 'calm'], recommendedFor: ['creative', 'pet', 'puzzle'] },
colors: ['#fff8e7', '#f0d9a0', '#c97b3a', '#8b4513'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], H.darken(c[0], 0.05)], 90);
// Checkered floor
var tileSize = 30;
for (var ty = h * 0.75; ty < h; ty += tileSize) {
for (var tx = 0; tx < w; tx += tileSize) {
var checker = ((Math.floor(tx / tileSize) + Math.floor(ty / tileSize)) % 2 === 0);
Draw.rect(ctx, tx, ty, tileSize, tileSize, checker ? '#e8e0d0' : '#c8b8a0');
}
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(404);
// Upper cabinets
for (var i = 0; i < 4; i++) {
var cx = w * 0.05 + i * w * 0.24;
Draw.rect(ctx, cx, h * 0.08, w * 0.2, h * 0.2, c[2]);
Draw.rect(ctx, cx + 3, h * 0.1, w * 0.2 - 6, h * 0.16, H.lighten(c[2], 0.15));
Draw.circle(ctx, cx + w * 0.1, h * 0.18, 3, '#brass');
Draw.circle(ctx, cx + w * 0.1, h * 0.18, 3, '#aa8844');
}
// Counter
Draw.rect(ctx, 0, h * 0.42, w, h * 0.04, '#ddd0bb');
Draw.rect(ctx, 0, h * 0.46, w, h * 0.29, c[3]);
// Stove
var sx = w * 0.35;
Draw.rect(ctx, sx, h * 0.32, w * 0.2, h * 0.1, '#444');
for (var b = 0; b < 4; b++) {
var bx = sx + w * 0.025 + b * w * 0.045;
var burnerGlow = Math.sin(t * 1.5 + b * 0.8) > 0.5 ? 0.4 : 0;
Draw.circle(ctx, bx, h * 0.37, 8, H.rgba('#ff4400', burnerGlow));
Draw.circle(ctx, bx, h * 0.37, 12, '#555');
ctx.strokeStyle = '#666'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(bx, h * 0.37, 10, 0, Math.PI * 2); ctx.stroke();
}
// Fridge
Draw.rect(ctx, w * 0.82, h * 0.15, w * 0.15, h * 0.6, '#d8d8d8');
Draw.rect(ctx, w * 0.83, h * 0.16, w * 0.13, h * 0.3, '#e8e8e8');
Draw.rect(ctx, w * 0.83, h * 0.48, w * 0.13, h * 0.25, '#e8e8e8');
Draw.rect(ctx, w * 0.95, h * 0.28, 3, 20, '#999');
Draw.rect(ctx, w * 0.95, h * 0.58, 3, 20, '#999');
// Sink
Draw.rect(ctx, w * 0.1, h * 0.35, w * 0.15, h * 0.07, '#bbb');
Draw.rect(ctx, w * 0.15, h * 0.3, 4, h * 0.05, '#999');
// Hanging pots
for (var p = 0; p < 3; p++) {
var px = w * 0.62 + p * w * 0.06;
var sway = Math.sin(t * 0.3 + p) * 2;
ctx.strokeStyle = '#777'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(px, h * 0.08); ctx.lineTo(px + sway, h * 0.14); ctx.stroke();
Draw.circle(ctx, px + sway, h * 0.16, 10 + p * 2, H.pick(['#884422', '#666', '#aa6633']));
}
// Window light
Draw.rect(ctx, w * 0.03, h * 0.08, w * 0.12, h * 0.18, '#aaddff');
ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 3;
ctx.strokeRect(w * 0.03, h * 0.08, w * 0.12, h * 0.18);
},
particles: { type: 'dust', count: 12, color: '#EECC88' }
},
arcade: {
meta: { id: 'indoor_arcade', mood: 'energetic', colors: ['#0d0d1a', '#1a0a2e', '#ff00ff', '#00ffff'], tags: ['neon', 'retro', 'gaming', 'dark'], description: 'Dark arcade with neon-lit cabinets, glowing screens, and retro vibes', compatibleMusicMoods: ['energetic', 'retro', 'electronic'], recommendedFor: ['action', 'racing', 'sports'] },
colors: ['#0d0d1a', '#1a0a2e', '#ff00ff', '#00ffff'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Dark reflective floor
Draw.rect(ctx, 0, h * 0.8, w, h * 0.2, '#0a0a15');
Draw.grid(ctx, w, h * 0.2, 40, H.rgba(c[2], 0.05), 1);
// Neon strip on ceiling
var neonPulse = 0.3 + Math.sin(t * 1.2) * 0.15;
Draw.rect(ctx, 0, 0, w, 4, H.rgba(c[2], neonPulse));
Draw.rect(ctx, 0, h * 0.8, w, 2, H.rgba(c[3], neonPulse * 0.6));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(505);
// Arcade cabinets
for (var i = 0; i < 6; i++) {
var ax = w * 0.05 + i * w * 0.155;
var cabColor = H.pick(['#222233', '#1a1a2e', '#2a1a2e']);
Draw.rect(ctx, ax, h * 0.3, w * 0.1, h * 0.5, cabColor);
// Screen glow
var screenColor = H.pick([c[2], c[3], '#44ff44', '#ffaa00']);
var flicker = 0.5 + Math.sin(t * (3 + rng()) + i * 2) * 0.2;
Draw.rect(ctx, ax + 5, h * 0.33, w * 0.1 - 10, h * 0.18, H.rgba(screenColor, flicker));
Draw.glow(ctx, ax + w * 0.05, h * 0.42, 40, screenColor);
// Control panel
Draw.rect(ctx, ax + 3, h * 0.55, w * 0.1 - 6, h * 0.08, '#333');
// Joystick
Draw.circle(ctx, ax + w * 0.03, h * 0.59, 4, '#888');
// Buttons
for (var b = 0; b < 3; b++) {
Draw.circle(ctx, ax + w * 0.06 + b * 10, h * 0.59, 3, H.pick(['#ff3333', '#33ff33', '#3333ff']));
}
}
// Neon signs on wall
ctx.font = 'bold 16px monospace';
var signGlow = 0.6 + Math.sin(t * 2) * 0.3;
ctx.fillStyle = H.rgba(c[2], signGlow);
ctx.shadowColor = c[2]; ctx.shadowBlur = 15;
ctx.fillText('PLAY', w * 0.15, h * 0.15);
ctx.fillStyle = H.rgba(c[3], signGlow);
ctx.shadowColor = c[3];
ctx.fillText('HIGH SCORE', w * 0.55, h * 0.15);
ctx.shadowBlur = 0;
// Floor reflections
for (var r = 0; r < 6; r++) {
var rx = w * 0.05 + r * w * 0.155 + w * 0.05;
var refColor = H.pick([c[2], c[3], '#44ff44']);
Draw.glow(ctx, rx, h * 0.85, 25, refColor);
}
// Scanlines
Draw.scanlines(ctx, w, h, 3, H.rgba('#000000', 0.08));
},
particles: { type: 'sparkles', count: 20, color: '#ff00ff' }
},
office: {
meta: { id: 'indoor_office', mood: 'focused', colors: ['#e8e4dc', '#c5bfb2', '#4a6fa5', '#2c3e50'], tags: ['work', 'modern', 'clean', 'professional'], description: 'Modern office with monitor glow, clean desks, and large windows', compatibleMusicMoods: ['calm', 'focused', 'ambient'], recommendedFor: ['puzzle', 'trivia', 'creative'] },
colors: ['#e8e4dc', '#c5bfb2', '#4a6fa5', '#2c3e50'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], H.darken(c[0], 0.08)], 90);
Draw.rect(ctx, 0, h * 0.8, w, h * 0.2, '#b0a898');
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(606);
// Large windows
for (var i = 0; i < 3; i++) {
var wx = w * 0.05 + i * w * 0.32;
Draw.rect(ctx, wx, h * 0.05, w * 0.25, h * 0.4, '#aaccee');
ctx.strokeStyle = '#ddd'; ctx.lineWidth = 4;
ctx.strokeRect(wx, h * 0.05, w * 0.25, h * 0.4);
ctx.beginPath(); ctx.moveTo(wx + w * 0.125, h * 0.05); ctx.lineTo(wx + w * 0.125, h * 0.45); ctx.stroke();
// Sky through window
Draw.circle(ctx, wx + w * 0.18, h * 0.15, 8, '#fff8dd');
}
// Desks with monitors
for (var d = 0; d < 3; d++) {
var dx = w * 0.08 + d * w * 0.32;
// Desk
Draw.rect(ctx, dx, h * 0.6, w * 0.22, h * 0.03, '#8b7355');
Draw.rect(ctx, dx + 5, h * 0.63, 4, h * 0.17, '#7a6245');
Draw.rect(ctx, dx + w * 0.19, h * 0.63, 4, h * 0.17, '#7a6245');
// Monitor
var monX = dx + w * 0.05;
Draw.rect(ctx, monX, h * 0.48, w * 0.12, h * 0.1, '#222');
var screenGlow = 0.6 + Math.sin(t * 0.5 + d) * 0.15;
Draw.rect(ctx, monX + 3, h * 0.49, w * 0.12 - 6, h * 0.08, H.rgba(c[2], screenGlow));
Draw.rect(ctx, monX + w * 0.055, h * 0.58, 6, h * 0.02, '#333');
Draw.rect(ctx, monX + w * 0.035, h * 0.598, w * 0.05, 3, '#333');
// Keyboard
Draw.rect(ctx, dx + w * 0.04, h * 0.585, w * 0.08, h * 0.012, '#444');
}
// Ceiling lights
for (var l = 0; l < 4; l++) {
var lx = w * 0.12 + l * w * 0.23;
Draw.rect(ctx, lx, 0, w * 0.12, 6, '#eee');
var lightFlicker = 0.12 + Math.sin(t * 0.3 + l * 0.5) * 0.03;
ctx.fillStyle = H.rgba('#FFFFFF', lightFlicker);
ctx.beginPath(); ctx.moveTo(lx, 6); ctx.lineTo(lx + w * 0.12, 6);
ctx.lineTo(lx + w * 0.18, h * 0.5); ctx.lineTo(lx - w * 0.06, h * 0.5); ctx.closePath(); ctx.fill();
}
// Plant in corner
Draw.rect(ctx, w * 0.92, h * 0.6, 14, h * 0.2, '#8b6914');
Draw.circle(ctx, w * 0.928, h * 0.55, 20, '#3a7a3a');
Draw.circle(ctx, w * 0.94, h * 0.5, 14, '#4a8a4a');
Draw.circle(ctx, w * 0.916, h * 0.48, 12, '#3a7a3a');
},
particles: { type: 'dust', count: 10, color: '#DDDDCC' }
},
library: {
meta: { id: 'indoor_library', mood: 'serene', colors: ['#2c1810', '#4a3020', '#c4956a', '#f4e4c1'], tags: ['books', 'quiet', 'knowledge', 'wood'], description: 'Grand library with towering bookshelves, warm wood, and reading lamps', compatibleMusicMoods: ['calm', 'serene', 'classical'], recommendedFor: ['story', 'trivia', 'puzzle', 'rpg'] },
colors: ['#2c1810', '#4a3020', '#c4956a', '#f4e4c1'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [H.darken(c[0], 0.2), c[0]], 90);
Draw.rect(ctx, 0, h * 0.85, w, h * 0.15, H.darken(c[1], 0.3));
// Wooden floor planks
for (var p = 0; p < 12; p++) {
var px = p * (w / 12);
ctx.strokeStyle = H.rgba('#000', 0.1);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(px, h * 0.85); ctx.lineTo(px, h); ctx.stroke();
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(707);
var bookColors = ['#8b2500', '#2e5090', '#1a6030', '#8b6914', '#6a1b6a', '#cc5533', '#336677'];
// Tall bookshelves on sides
for (var side = 0; side < 2; side++) {
var sx = side === 0 ? 0 : w * 0.78;
var sw = w * 0.22;
Draw.rect(ctx, sx, h * 0.02, sw, h * 0.83, c[1]);
// Shelves
for (var sh = 0; sh < 7; sh++) {
var sy = h * 0.05 + sh * h * 0.115;
Draw.rect(ctx, sx, sy + h * 0.1, sw, 4, H.darken(c[1], 0.2));
// Books
var bx = sx + 5;
while (bx < sx + sw - 8) {
var bw = 5 + rng() * 10;
var bh = h * 0.08 + rng() * h * 0.02;
var bc = bookColors[Math.floor(rng() * bookColors.length)];
Draw.rect(ctx, bx, sy + h * 0.1 - bh, bw, bh, bc);
bx += bw + 1;
}
}
}
// Center reading area
Draw.rect(ctx, w * 0.35, h * 0.65, w * 0.3, h * 0.04, c[2]);
Draw.rect(ctx, w * 0.4, h * 0.69, 5, h * 0.16, H.darken(c[2], 0.3));
Draw.rect(ctx, w * 0.58, h * 0.69, 5, h * 0.16, H.darken(c[2], 0.3));
// Open book on table
Draw.rect(ctx, w * 0.44, h * 0.62, w * 0.06, h * 0.03, c[3]);
Draw.rect(ctx, w * 0.5, h * 0.62, w * 0.06, h * 0.03, H.darken(c[3], 0.05));
// Reading lamps
for (var l = 0; l < 2; l++) {
var lx = w * 0.38 + l * w * 0.24;
Draw.rect(ctx, lx, h * 0.5, 3, h * 0.15, '#886644');
var lampGlow = 0.25 + Math.sin(t * 0.4 + l * Math.PI) * 0.08;
Draw.polygon(ctx, lx + 1, h * 0.49, 14, 3, '#aa8855', -Math.PI / 2);
Draw.glow(ctx, lx + 1, h * 0.47, 50, '#ffcc66');
ctx.fillStyle = H.rgba('#ffdd88', lampGlow);
ctx.beginPath();
ctx.moveTo(lx - 18, h * 0.5); ctx.lineTo(lx + 20, h * 0.5);
ctx.lineTo(lx + 40, h * 0.85); ctx.lineTo(lx - 38, h * 0.85);
ctx.closePath(); ctx.fill();
}
// Arched window in back center
var awx = w * 0.4, awy = h * 0.08, aww = w * 0.2, awh = h * 0.35;
Draw.rect(ctx, awx, awy + awh * 0.3, aww, awh * 0.7, '#334466');
ctx.fillStyle = '#334466';
ctx.beginPath(); ctx.arc(awx + aww / 2, awy + awh * 0.3, aww / 2, Math.PI, 0); ctx.fill();
ctx.strokeStyle = c[2]; ctx.lineWidth = 4;
ctx.beginPath(); ctx.arc(awx + aww / 2, awy + awh * 0.3, aww / 2, Math.PI, 0); ctx.stroke();
ctx.strokeRect(awx, awy + awh * 0.3, aww, awh * 0.7);
// Moonlight through window
Draw.glow(ctx, awx + aww / 2, awy + awh * 0.3, 60, '#6688aa');
},
particles: { type: 'dust', count: 15, color: '#CCBB88' }
},
gym: {
meta: { id: 'indoor_gym', mood: 'energetic', colors: ['#1a1a24', '#2a2a3a', '#ff4444', '#ff8800'], tags: ['fitness', 'sports', 'energy', 'active'], description: 'Modern gym with equipment silhouettes, rubber floor, and bright overhead lights', compatibleMusicMoods: ['energetic', 'intense', 'upbeat'], recommendedFor: ['action', 'sports', 'racing'] },
colors: ['#1a1a24', '#2a2a3a', '#ff4444', '#ff8800'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Rubber floor with subtle texture
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, '#222230');
var rng = H.seededRandom(808);
for (var i = 0; i < 40; i++) {
Draw.circle(ctx, rng() * w, h * 0.78 + rng() * h * 0.22, 1, H.rgba('#333344', 0.5));
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(809);
// Wall mirror
Draw.rect(ctx, w * 0.1, h * 0.1, w * 0.8, h * 0.45, H.rgba('#445566', 0.15));
ctx.strokeStyle = H.rgba('#666', 0.3); ctx.lineWidth = 3;
ctx.strokeRect(w * 0.1, h * 0.1, w * 0.8, h * 0.45);
// Dumbbells rack
for (var d = 0; d < 6; d++) {
var dx = w * 0.05 + d * w * 0.05;
var dy = h * 0.7;
Draw.rect(ctx, dx, dy, 8, 4, '#555');
Draw.circle(ctx, dx - 2, dy + 2, 5, '#444');
Draw.circle(ctx, dx + 10, dy + 2, 5, '#444');
}
// Bench press
Draw.rect(ctx, w * 0.55, h * 0.65, w * 0.15, h * 0.02, '#444');
Draw.rect(ctx, w * 0.57, h * 0.67, 4, h * 0.11, '#555');
Draw.rect(ctx, w * 0.68, h * 0.67, 4, h * 0.11, '#555');
// Barbell
Draw.rect(ctx, w * 0.5, h * 0.6, w * 0.25, 3, '#888');
Draw.rect(ctx, w * 0.5, h * 0.585, 8, 18, '#666');
Draw.rect(ctx, w * 0.73, h * 0.585, 8, 18, '#666');
// Treadmill
Draw.rect(ctx, w * 0.8, h * 0.55, w * 0.12, h * 0.23, '#333');
Draw.rect(ctx, w * 0.81, h * 0.63, w * 0.1, h * 0.13, '#222');
var beltMove = (t * 2) % 1;
for (var s = 0; s < 6; s++) {
var sy = h * 0.63 + (s / 6 + beltMove / 6) * h * 0.13;
if (sy < h * 0.76) Draw.rect(ctx, w * 0.81, sy, w * 0.1, 1, H.rgba('#444', 0.5));
}
// Overhead lights
for (var l = 0; l < 5; l++) {
var lx = w * 0.1 + l * w * 0.2;
Draw.rect(ctx, lx, 0, w * 0.08, 8, '#eee');
var intensity = 0.08 + Math.sin(t * 0.2 + l * 0.3) * 0.02;
ctx.fillStyle = H.rgba('#FFFFFF', intensity);
ctx.beginPath(); ctx.moveTo(lx, 8); ctx.lineTo(lx + w * 0.08, 8);
ctx.lineTo(lx + w * 0.14, h * 0.78); ctx.lineTo(lx - w * 0.06, h * 0.78);
ctx.closePath(); ctx.fill();
}
// Red accent stripe on wall
var stripeGlow = 0.6 + Math.sin(t * 0.8) * 0.2;
Draw.rect(ctx, 0, h * 0.55, w, 4, H.rgba(c[2], stripeGlow));
// Motivational poster (abstract rectangle)
Draw.rect(ctx, w * 0.38, h * 0.15, w * 0.12, h * 0.15, '#333');
Draw.rect(ctx, w * 0.39, h * 0.16, w * 0.1, h * 0.13, c[2]);
},
particles: { type: 'dust', count: 8, color: '#887766' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,545 @@
// mechanical.js — Mechanical/industrial theme for procedural background system
// 8 variants: gears, circuits, factory, robotFactory, conveyorBelts, pipes, steamPunk, techLab
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
// Helper: draw a gear shape with teeth
function drawGear(ctx, cx, cy, innerR, outerR, teeth, rotation, color) {
ctx.fillStyle = color;
ctx.beginPath();
for (var i = 0; i < teeth * 2; i++) {
var angle = rotation + (i / (teeth * 2)) * Math.PI * 2;
var r = i % 2 === 0 ? outerR : innerR;
var x = cx + Math.cos(angle) * r;
var y = cy + Math.sin(angle) * r;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
// Center hole
ctx.fillStyle = H.darken(color, 0.5);
ctx.beginPath();
ctx.arc(cx, cy, innerR * 0.35, 0, Math.PI * 2);
ctx.fill();
}
// Helper: draw a rivet
function drawRivet(ctx, x, y, r, color) {
var g = D.radialGradient(ctx, x - r * 0.3, y - r * 0.3, r, [[0, H.lighten(color, 0.4)], [0.6, color], [1, H.darken(color, 0.3)]]);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
// Helper: draw a pipe segment
function drawPipe(ctx, x1, y1, x2, y2, width, color) {
var angle = Math.atan2(y2 - y1, x2 - x1);
var perpX = Math.sin(angle) * width / 2;
var perpY = -Math.cos(angle) * width / 2;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x1 + perpX, y1 + perpY);
ctx.lineTo(x2 + perpX, y2 + perpY);
ctx.lineTo(x2 - perpX, y2 - perpY);
ctx.lineTo(x1 - perpX, y1 - perpY);
ctx.closePath();
ctx.fill();
// Highlight stripe
ctx.fillStyle = H.rgba(H.lighten(color, 0.3), 0.4);
ctx.beginPath();
ctx.moveTo(x1 + perpX * 0.6, y1 + perpY * 0.6);
ctx.lineTo(x2 + perpX * 0.6, y2 + perpY * 0.6);
ctx.lineTo(x2 + perpX * 0.2, y2 + perpY * 0.2);
ctx.lineTo(x1 + perpX * 0.2, y1 + perpY * 0.2);
ctx.closePath();
ctx.fill();
}
// Helper: draw a gauge
function drawGauge(ctx, cx, cy, r, needleAngle, color, rimColor) {
// Rim
ctx.strokeStyle = rimColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
// Face
ctx.fillStyle = H.darken(color, 0.6);
ctx.beginPath();
ctx.arc(cx, cy, r - 2, 0, Math.PI * 2);
ctx.fill();
// Tick marks
ctx.strokeStyle = H.rgba(rimColor, 0.5);
ctx.lineWidth = 1;
for (var i = 0; i < 8; i++) {
var a = (i / 8) * Math.PI * 2 - Math.PI / 2;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * (r * 0.7), cy + Math.sin(a) * (r * 0.7));
ctx.lineTo(cx + Math.cos(a) * (r * 0.9), cy + Math.sin(a) * (r * 0.9));
ctx.stroke();
}
// Needle
ctx.strokeStyle = '#CC3333';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(needleAngle) * r * 0.75, cy + Math.sin(needleAngle) * r * 0.75);
ctx.stroke();
// Center dot
D.circle(ctx, cx, cy, 3, rimColor);
}
engine.registerTheme('mechanical', {
gears: {
meta: { id: 'mechanical_gears', mood: 'focused', colors: ['#1A1A24', '#2A2A38', '#7A7A88', '#C08040'], tags: ['industrial', 'moving', 'metallic'], description: 'Interlocking gears turning against a dark steel backdrop', compatibleMusicMoods: ['ambient', 'electronic', 'tense'], recommendedFor: ['puzzle', 'physics', 'rpg'] },
colors: ['#1A1A24', '#2A2A38', '#7A7A88', '#C08040'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[0]], 90);
// Subtle metal texture via scanlines
Draw.scanlines(ctx, w, h, 3, H.rgba(c[2], 0.03));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(100);
// Draw interlocking gears at various sizes
var gearData = [];
for (var i = 0; i < 9; i++) {
gearData.push({
x: rng() * w, y: rng() * h,
inner: 15 + rng() * 40, outer: 30 + rng() * 55,
teeth: 8 + Math.floor(rng() * 10),
speed: (rng() - 0.5) * 0.8, shade: rng()
});
}
for (var i = 0; i < gearData.length; i++) {
var g = gearData[i];
var col = H.blendColor(c[2], c[3], g.shade * 0.5);
drawGear(ctx, g.x, g.y, g.inner, g.outer, g.teeth, t * g.speed, col);
}
// Small accent gears in corners
drawGear(ctx, w * 0.08, h * 0.08, 10, 18, 6, -t * 0.6, H.rgba(c[3], 0.6));
drawGear(ctx, w * 0.92, h * 0.92, 12, 22, 7, t * 0.5, H.rgba(c[3], 0.6));
// Rivets along edges
for (var i = 0; i < 12; i++) {
drawRivet(ctx, 15 + i * (w - 30) / 11, 12, 3, c[2]);
drawRivet(ctx, 15 + i * (w - 30) / 11, h - 12, 3, c[2]);
}
},
particles: { type: 'dust', count: 20, color: '#998877' }
},
circuits: {
meta: { id: 'mechanical_circuits', mood: 'focused', colors: ['#0A0F14', '#112222', '#00CC88', '#33FFAA'], tags: ['digital', 'tech', 'grid'], description: 'Circuit board traces with glowing data paths', compatibleMusicMoods: ['electronic', 'ambient', 'tense'], recommendedFor: ['puzzle', 'trivia', 'physics'] },
colors: ['#0A0F14', '#112222', '#00CC88', '#33FFAA'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.grid(ctx, w, h, 40, H.rgba(c[2], 0.06), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(150);
ctx.lineWidth = 2;
// Draw circuit traces
for (var i = 0; i < 20; i++) {
var sx = rng() * w, sy = rng() * h;
var segments = 3 + Math.floor(rng() * 5);
var pulse = 0.3 + Math.sin(t * 1.5 + i * 0.7) * 0.25;
ctx.strokeStyle = H.rgba(c[2], 0.15 + pulse * 0.3);
ctx.beginPath();
ctx.moveTo(sx, sy);
for (var j = 0; j < segments; j++) {
var dir = Math.floor(rng() * 2); // 0=horizontal, 1=vertical
var len = 20 + rng() * 80;
if (dir === 0) sx += (rng() > 0.5 ? 1 : -1) * len;
else sy += (rng() > 0.5 ? 1 : -1) * len;
sx = H.clamp(sx, 10, w - 10);
sy = H.clamp(sy, 10, h - 10);
ctx.lineTo(sx, sy);
}
ctx.stroke();
// Node at end
D.circle(ctx, sx, sy, 3, H.rgba(c[3], 0.4 + pulse * 0.4));
}
// Chip packages
for (var i = 0; i < 6; i++) {
var cx = rng() * w * 0.8 + w * 0.1;
var cy = rng() * h * 0.8 + h * 0.1;
var cw = 20 + rng() * 20, ch = 15 + rng() * 15;
Draw.rect(ctx, cx - cw / 2, cy - ch / 2, cw, ch, H.rgba(c[1], 0.8));
ctx.strokeStyle = H.rgba(c[2], 0.3);
ctx.lineWidth = 1;
ctx.strokeRect(cx - cw / 2, cy - ch / 2, cw, ch);
// Pins
for (var p = 0; p < 4; p++) {
Draw.rect(ctx, cx - cw / 2 - 4, cy - ch / 2 + 3 + p * (ch / 4), 4, 2, H.rgba(c[2], 0.3));
Draw.rect(ctx, cx + cw / 2, cy - ch / 2 + 3 + p * (ch / 4), 4, 2, H.rgba(c[2], 0.3));
}
}
// Data pulses traveling along traces
for (var i = 0; i < 8; i++) {
var px = (rng() * w + t * 60 * (rng() > 0.5 ? 1 : -1)) % w;
var py = Math.round(rng() * h / 40) * 40;
D.glow(ctx, px, py, 6, H.rgba(c[3], 0.5 + Math.sin(t * 3 + i) * 0.3));
}
},
particles: { type: 'sparkles', count: 25, color: '#33FFAA' }
},
factory: {
meta: { id: 'mechanical_factory', mood: 'busy', colors: ['#1A1510', '#2A2520', '#8B7355', '#DDAA44'], tags: ['industrial', 'gritty', 'warm'], description: 'Factory floor with smokestacks and warning stripes', compatibleMusicMoods: ['tense', 'electronic', 'action'], recommendedFor: ['action', 'physics', 'puzzle'] },
colors: ['#1A1510', '#2A2520', '#8B7355', '#DDAA44'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.darken(c[0], 0.3)], 90);
// Floor
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, H.darken(c[2], 0.5));
// Floor grid
Draw.grid(ctx, w, h * 0.22, 30, H.rgba(c[2], 0.08), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(200);
// Smokestacks in back
for (var i = 0; i < 3; i++) {
var sx = w * 0.2 + i * w * 0.3;
var sw = 25 + rng() * 15;
var sh = h * 0.35 + rng() * h * 0.1;
Draw.rect(ctx, sx - sw / 2, h * 0.78 - sh, sw, sh, H.darken(c[2], 0.3));
// Warning stripes
ctx.save();
ctx.beginPath();
ctx.rect(sx - sw / 2, h * 0.78 - sh, sw, 12);
ctx.clip();
for (var s = -2; s < sw / 8 + 2; s++) {
Draw.rect(ctx, sx - sw / 2 + s * 16, h * 0.78 - sh, 8, 12, c[3]);
}
ctx.restore();
// Smoke puffs
for (var j = 0; j < 3; j++) {
var smokeY = h * 0.78 - sh - 15 - j * 25;
var drift = Math.sin(t * 0.5 + i + j) * 10;
var alpha = 0.08 - j * 0.02;
ctx.fillStyle = H.rgba('#888888', alpha);
ctx.beginPath();
ctx.ellipse(sx + drift, smokeY, 15 + j * 10, 8 + j * 5, 0, 0, Math.PI * 2);
ctx.fill();
}
}
// Catwalk
Draw.rect(ctx, 0, h * 0.5, w, 4, c[2]);
ctx.strokeStyle = H.rgba(c[2], 0.4);
ctx.lineWidth = 1;
for (var i = 0; i < w; i += 15) {
ctx.beginPath();
ctx.moveTo(i, h * 0.5 + 4);
ctx.lineTo(i + 7, h * 0.5 + 18);
ctx.lineTo(i + 15, h * 0.5 + 4);
ctx.stroke();
}
Draw.rect(ctx, 0, h * 0.5 + 18, w, 2, H.rgba(c[2], 0.3));
// Warning light
var blink = Math.sin(t * 4) > 0 ? 0.8 : 0.1;
D.glow(ctx, w * 0.9, h * 0.15, 25, H.rgba('#FF4400', blink * 0.4));
D.circle(ctx, w * 0.9, h * 0.15, 6, H.rgba('#FF4400', blink));
// Rivets on floor seam
for (var i = 0; i < 16; i++) {
drawRivet(ctx, w * 0.05 + i * (w * 0.9 / 15), h * 0.78, 2.5, c[2]);
}
},
particles: { type: 'dust', count: 30, color: '#AA9977' }
},
robotFactory: {
meta: { id: 'mechanical_robotFactory', mood: 'playful', colors: ['#151520', '#222238', '#5588CC', '#FFAA33'], tags: ['robots', 'tech', 'fun'], description: 'Robot assembly line with mechanical arms and sparks', compatibleMusicMoods: ['electronic', 'energetic', 'playful'], recommendedFor: ['action', 'puzzle', 'creative'] },
colors: ['#151520', '#222238', '#5588CC', '#FFAA33'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Floor
Draw.rect(ctx, 0, h * 0.82, w, h * 0.18, H.darken(c[1], 0.3));
Draw.grid(ctx, w, h, 50, H.rgba(c[2], 0.04), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(250);
// Assembly arms from ceiling
for (var i = 0; i < 4; i++) {
var ax = w * 0.15 + i * w * 0.23;
var armAngle = Math.sin(t * 0.7 + i * 1.5) * 0.4;
var armLen = h * 0.25;
var elbowX = ax + Math.sin(armAngle) * armLen;
var elbowY = armLen * Math.cos(armAngle);
// Arm segment
ctx.strokeStyle = c[2];
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(ax, 0);
ctx.lineTo(elbowX, elbowY);
ctx.stroke();
// Joint
D.circle(ctx, elbowX, elbowY, 6, H.lighten(c[2], 0.2));
D.circle(ctx, ax, 0, 8, c[2]);
// Claw tip
var clawAngle = armAngle + Math.sin(t * 1.5 + i) * 0.3;
var tipX = elbowX + Math.sin(clawAngle) * 20;
var tipY = elbowY + Math.cos(clawAngle) * 20;
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(elbowX, elbowY);
ctx.lineTo(tipX, tipY);
ctx.stroke();
// Sparks at claw
if (Math.sin(t * 5 + i * 2.3) > 0.6) {
D.glow(ctx, tipX, tipY, 10, H.rgba(c[3], 0.6));
}
}
// Robot bodies on conveyor (silhouettes)
for (var i = 0; i < 3; i++) {
var rx = (w * 0.1 + i * w * 0.35 + t * 15) % (w + 60) - 30;
var ry = h * 0.82;
// Body
Draw.rect(ctx, rx - 12, ry - 35, 24, 25, c[2]);
// Head
Draw.rect(ctx, rx - 8, ry - 45, 16, 12, H.lighten(c[2], 0.15));
// Eyes
var eyeGlow = 0.4 + Math.sin(t * 3 + i) * 0.3;
D.circle(ctx, rx - 4, ry - 40, 2, H.rgba(c[3], eyeGlow));
D.circle(ctx, rx + 4, ry - 40, 2, H.rgba(c[3], eyeGlow));
// Legs
Draw.rect(ctx, rx - 10, ry - 10, 6, 10, H.darken(c[2], 0.2));
Draw.rect(ctx, rx + 4, ry - 10, 6, 10, H.darken(c[2], 0.2));
}
// Conveyor belt
Draw.rect(ctx, 0, h * 0.82, w, 5, H.darken(c[2], 0.4));
var beltOffset = (t * 30) % 20;
for (var i = -1; i < w / 20 + 1; i++) {
Draw.rect(ctx, i * 20 + beltOffset, h * 0.82, 2, 5, H.rgba(c[2], 0.3));
}
},
particles: { type: 'sparkles', count: 20, color: '#FFAA33' }
},
conveyorBelts: {
meta: { id: 'mechanical_conveyorBelts', mood: 'busy', colors: ['#18181F', '#28283A', '#666680', '#DD8822'], tags: ['movement', 'industrial', 'repetitive'], description: 'Layered conveyor belts moving crates and parts', compatibleMusicMoods: ['electronic', 'ambient', 'action'], recommendedFor: ['puzzle', 'action', 'physics'] },
colors: ['#18181F', '#28283A', '#666680', '#DD8822'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.scanlines(ctx, w, h, 4, H.rgba(c[2], 0.02));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(300);
// Three conveyor belt layers
var beltYs = [h * 0.3, h * 0.55, h * 0.8];
var beltSpeeds = [25, -35, 20];
for (var b = 0; b < 3; b++) {
var by = beltYs[b];
var beltH = 8;
// Belt surface
Draw.rect(ctx, 0, by, w, beltH, H.darken(c[2], 0.3));
// Belt treads
var treadOffset = (t * beltSpeeds[b]) % 16;
for (var i = -1; i < w / 16 + 1; i++) {
Draw.rect(ctx, i * 16 + treadOffset, by, 2, beltH, H.rgba(c[2], 0.2));
}
// Support legs
for (var i = 0; i < 5; i++) {
var lx = w * 0.1 + i * w * 0.2;
Draw.rect(ctx, lx - 3, by + beltH, 6, h - by - beltH, H.darken(c[2], 0.5));
}
// Rollers at ends
D.circle(ctx, 15, by + beltH / 2, beltH / 2 + 2, c[2]);
D.circle(ctx, w - 15, by + beltH / 2, beltH / 2 + 2, c[2]);
// Items on belt
for (var i = 0; i < 4; i++) {
var itemX = (rng() * w + t * beltSpeeds[b]) % (w + 40) - 20;
var itemW = 12 + rng() * 18;
var itemH = 10 + rng() * 14;
var itemCol = rng() > 0.5 ? H.blendColor(c[2], c[3], rng() * 0.6) : H.darken(c[2], rng() * 0.3);
Draw.rect(ctx, itemX - itemW / 2, by - itemH, itemW, itemH, itemCol);
}
}
// Overhead arrows
ctx.strokeStyle = H.rgba(c[3], 0.2);
ctx.lineWidth = 1.5;
for (var i = 0; i < 6; i++) {
var ax = w * 0.1 + i * w * 0.16;
ctx.beginPath();
ctx.moveTo(ax, h * 0.12);
ctx.lineTo(ax + 10, h * 0.15);
ctx.lineTo(ax, h * 0.18);
ctx.stroke();
}
},
particles: { type: 'dust', count: 15, color: '#887766' }
},
pipes: {
meta: { id: 'mechanical_pipes', mood: 'complex', colors: ['#121218', '#1E2A1E', '#558855', '#88BB44'], tags: ['plumbing', 'industrial', 'network'], description: 'Interconnected pipe network with valves and joints', compatibleMusicMoods: ['ambient', 'tense', 'electronic'], recommendedFor: ['puzzle', 'physics', 'rpg'] },
colors: ['#121218', '#1E2A1E', '#558855', '#88BB44'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(350);
// Horizontal pipes
var pipeYs = [h * 0.15, h * 0.35, h * 0.55, h * 0.75, h * 0.9];
for (var i = 0; i < pipeYs.length; i++) {
var pw = 8 + rng() * 8;
var pipeCol = H.blendColor(c[2], c[3], rng() * 0.4);
drawPipe(ctx, 0, pipeYs[i], w, pipeYs[i], pw, pipeCol);
// Joints/flanges
for (var j = 0; j < 4; j++) {
var jx = rng() * w;
Draw.rect(ctx, jx - pw * 0.7, pipeYs[i] - pw * 0.7, pw * 1.4, pw * 1.4, H.darken(pipeCol, 0.15));
}
}
// Vertical connector pipes
for (var i = 0; i < 7; i++) {
var vx = rng() * w * 0.8 + w * 0.1;
var fromY = pipeYs[Math.floor(rng() * (pipeYs.length - 1))];
var toIdx = Math.min(Math.floor(rng() * (pipeYs.length - 1)) + 1, pipeYs.length - 1);
var toY = pipeYs[toIdx];
if (fromY === toY) toY = pipeYs[Math.min(toIdx + 1, pipeYs.length - 1)];
var vpw = 6 + rng() * 5;
drawPipe(ctx, vx, fromY, vx, toY, vpw, H.blendColor(c[2], c[3], rng() * 0.3));
}
// Valves (wheel shapes)
for (var i = 0; i < 4; i++) {
var vx = rng() * w * 0.7 + w * 0.15;
var vy = pipeYs[Math.floor(rng() * pipeYs.length)];
var vr = 8 + rng() * 6;
var spin = t * 0.3 * (rng() > 0.5 ? 1 : -1);
ctx.strokeStyle = H.rgba(c[3], 0.7);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(vx, vy, vr, 0, Math.PI * 2);
ctx.stroke();
for (var s = 0; s < 4; s++) {
var sa = spin + s * Math.PI / 2;
ctx.beginPath();
ctx.moveTo(vx, vy);
ctx.lineTo(vx + Math.cos(sa) * vr, vy + Math.sin(sa) * vr);
ctx.stroke();
}
}
// Dripping animation
for (var i = 0; i < 3; i++) {
var dx = rng() * w;
var dripY = pipeYs[Math.floor(rng() * pipeYs.length)] + 8;
var dropY = dripY + ((t * 40 + rng() * 100) % 50);
var alpha = 1 - ((t * 40 + rng() * 100) % 50) / 50;
D.circle(ctx, dx, dropY, 2, H.rgba(c[3], alpha * 0.5));
}
},
particles: { type: 'dust', count: 12, color: '#667755' }
},
steamPunk: {
meta: { id: 'mechanical_steamPunk', mood: 'adventurous', colors: ['#1A1008', '#2A1A0A', '#C08040', '#E8C060'], tags: ['brass', 'vintage', 'ornate'], description: 'Brass gears, gauges, and steam vents in warm tones', compatibleMusicMoods: ['epic', 'ambient', 'action'], recommendedFor: ['rpg', 'story', 'action'] },
colors: ['#1A1008', '#2A1A0A', '#C08040', '#E8C060'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.darken(c[0], 0.2)], 135);
// Warm vignette
var g = D.radialGradient(ctx, w / 2, h / 2, Math.max(w, h) * 0.7, [[0, H.rgba(c[0], 0)], [1, H.rgba(c[0], 0.5)]]);
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(400);
// Large decorative gears
drawGear(ctx, w * 0.2, h * 0.3, 35, 55, 12, t * 0.15, H.rgba(c[2], 0.6));
drawGear(ctx, w * 0.35, h * 0.45, 20, 35, 8, -t * 0.2, H.rgba(c[3], 0.5));
drawGear(ctx, w * 0.8, h * 0.25, 40, 65, 14, t * 0.12, H.rgba(c[2], 0.55));
drawGear(ctx, w * 0.65, h * 0.7, 25, 42, 10, -t * 0.18, H.rgba(c[3], 0.5));
// Gauges
var needleWobble1 = -Math.PI / 4 + Math.sin(t * 0.8) * 0.5;
drawGauge(ctx, w * 0.5, h * 0.2, 22, needleWobble1, c[0], c[2]);
var needleWobble2 = Math.sin(t * 0.5) * 0.8;
drawGauge(ctx, w * 0.88, h * 0.6, 18, needleWobble2, c[0], c[3]);
// Decorative border rivets
for (var i = 0; i < 20; i++) {
drawRivet(ctx, w * 0.03, h * 0.05 + i * (h * 0.9 / 19), 3, c[2]);
drawRivet(ctx, w * 0.97, h * 0.05 + i * (h * 0.9 / 19), 3, c[2]);
}
for (var i = 0; i < 14; i++) {
drawRivet(ctx, w * 0.05 + i * (w * 0.9 / 13), h * 0.03, 3, c[2]);
drawRivet(ctx, w * 0.05 + i * (w * 0.9 / 13), h * 0.97, 3, c[2]);
}
// Steam vents
for (var v = 0; v < 2; v++) {
var vx = w * 0.3 + v * w * 0.4;
var vy = h * 0.85;
Draw.rect(ctx, vx - 6, vy, 12, h - vy, H.darken(c[2], 0.3));
// Steam puffs
for (var p = 0; p < 4; p++) {
var pAlpha = 0.06 - p * 0.012;
var drift = Math.sin(t + v + p * 0.5) * 8;
ctx.fillStyle = H.rgba('#CCBBAA', pAlpha);
ctx.beginPath();
ctx.ellipse(vx + drift, vy - 10 - p * 18, 12 + p * 8, 6 + p * 4, 0, 0, Math.PI * 2);
ctx.fill();
}
}
},
particles: { type: 'dust', count: 18, color: '#C0A060' }
},
techLab: {
meta: { id: 'mechanical_techLab', mood: 'curious', colors: ['#0E0E1A', '#1A1A30', '#4455AA', '#66DDFF'], tags: ['science', 'clean', 'futuristic'], description: 'Clean tech laboratory with holographic displays and instruments', compatibleMusicMoods: ['ambient', 'electronic', 'calm'], recommendedFor: ['puzzle', 'trivia', 'creative'] },
colors: ['#0E0E1A', '#1A1A30', '#4455AA', '#66DDFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Clean floor
Draw.rect(ctx, 0, h * 0.85, w, h * 0.15, H.darken(c[1], 0.2));
Draw.grid(ctx, w, h, 60, H.rgba(c[2], 0.04), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(450);
// Holographic display panels
for (var i = 0; i < 3; i++) {
var px = w * 0.12 + i * w * 0.32;
var py = h * 0.15;
var pw = w * 0.22, ph = h * 0.35;
// Panel frame
ctx.strokeStyle = H.rgba(c[2], 0.4);
ctx.lineWidth = 1.5;
ctx.strokeRect(px, py, pw, ph);
// Panel glow fill
ctx.fillStyle = H.rgba(c[2], 0.05 + Math.sin(t + i) * 0.02);
ctx.fillRect(px, py, pw, ph);
// Data lines inside panel
ctx.strokeStyle = H.rgba(c[3], 0.2);
ctx.lineWidth = 1;
for (var l = 0; l < 5; l++) {
var ly = py + 10 + l * (ph - 20) / 5;
ctx.beginPath();
ctx.moveTo(px + 8, ly);
for (var x = 0; x < pw - 16; x += 4) {
var val = Math.sin(x * 0.1 + t * 2 + l * 1.3 + i) * 6;
ctx.lineTo(px + 8 + x, ly + val);
}
ctx.stroke();
}
// Blinking status light
var statusBlink = Math.sin(t * 2.5 + i * 1.1) > 0 ? 0.8 : 0.2;
D.circle(ctx, px + pw - 8, py + 8, 3, H.rgba(c[3], statusBlink));
}
// Lab equipment silhouettes
for (var i = 0; i < 4; i++) {
var ex = rng() * w * 0.8 + w * 0.1;
var eH = 15 + rng() * 30;
Draw.rect(ctx, ex - 8, h * 0.85 - eH, 16, eH, H.rgba(c[2], 0.3));
D.circle(ctx, ex, h * 0.85 - eH - 5, 4, H.rgba(c[3], 0.3));
}
// Floating data points
for (var i = 0; i < 10; i++) {
var dx = rng() * w;
var dy = rng() * h * 0.8;
var flicker = 0.15 + Math.sin(t * 1.8 + rng() * 10) * 0.15;
D.circle(ctx, dx, dy, 1.5, H.rgba(c[3], flicker));
}
},
particles: { type: 'sparkles', count: 25, color: '#66DDFF' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,360 @@
// nature.js — Nature theme for procedural background system
// 8 variants: forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('nature', {
forest: {
meta: { id: 'nature_forest', mood: 'peaceful', colors: ['#1A3320', '#2D5A27', '#4A7A3F', '#8FB86A'], tags: ['trees', 'green', 'calm'], description: 'A dense forest with layered canopy and dappled light', compatibleMusicMoods: ['calm', 'ambient', 'dreamy'], recommendedFor: ['story', 'rpg', 'puzzle'] },
colors: ['#1A3320', '#2D5A27', '#4A7A3F', '#8FB86A'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#5588AA', '#88BBCC', c[1]], 90);
Draw.clouds(ctx, w, h, h * 0.08, 5, '#FFFFFF', 110);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Background tree layer
Draw.hills(ctx, w, h, h * 0.4, 8, H.darken(c[0], 0.3), 111);
Draw.trees(ctx, w, h, h * 0.5, 25, '#3D2B1F', c[0], 112);
// Mid tree layer
Draw.hills(ctx, w, h, h * 0.6, 6, c[1], 113);
Draw.trees(ctx, w, h, h * 0.68, 18, '#5C3A1E', c[2], 114);
// Foreground
Draw.hills(ctx, w, h, h * 0.8, 5, c[2], 115);
Draw.trees(ctx, w, h, h * 0.85, 10, '#6B4226', c[3], 116);
// Forest floor
Draw.rect(ctx, 0, h * 0.88, w, h * 0.12, H.darken(c[0], 0.4));
// Light rays
var rng = H.seededRandom(117);
ctx.save();
ctx.globalAlpha = 0.04 + Math.sin(t * 0.5) * 0.02;
for (var i = 0; i < 5; i++) {
var rx = rng() * w;
ctx.fillStyle = '#FFFFCC';
ctx.beginPath();
ctx.moveTo(rx, 0);
ctx.lineTo(rx - 30, h * 0.7);
ctx.lineTo(rx + 30, h * 0.7);
ctx.closePath();
ctx.fill();
}
ctx.restore();
},
particles: { type: 'fireflies', count: 20, color: '#CCFF66' }
},
jungle: {
meta: { id: 'nature_jungle', mood: 'adventurous', colors: ['#0D2B0D', '#1A4D1A', '#2D7A2D', '#55AA33'], tags: ['tropical', 'dense', 'lush'], description: 'Thick tropical jungle with hanging vines and humid air', compatibleMusicMoods: ['adventurous', 'tense', 'ambient'], recommendedFor: ['action', 'rpg', 'pet'] },
colors: ['#0D2B0D', '#1A4D1A', '#2D7A2D', '#55AA33'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#2A4420', c[0], H.darken(c[0], 0.3)], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
Draw.hills(ctx, w, h, h * 0.35, 10, c[1], 210);
Draw.trees(ctx, w, h, h * 0.5, 30, '#3B2510', c[1], 211);
// Vines
var rng = H.seededRandom(212);
ctx.strokeStyle = H.rgba(c[2], 0.6);
ctx.lineWidth = 2;
for (var i = 0; i < 12; i++) {
var vx = rng() * w;
var sway = Math.sin(t * 0.8 + i * 1.3) * 8;
ctx.beginPath();
ctx.moveTo(vx, 0);
ctx.quadraticCurveTo(vx + sway, h * 0.2 + rng() * h * 0.15, vx + sway * 0.5, h * 0.3 + rng() * h * 0.2);
ctx.stroke();
}
// Large canopy leaves at top
Draw.hills(ctx, w, h * 0.15, h * 0.02, 12, H.rgba(c[2], 0.5), 213);
// Mid layer dense foliage
Draw.trees(ctx, w, h, h * 0.7, 20, '#4B3520', c[2], 214);
// Ground ferns
Draw.hills(ctx, w, h, h * 0.85, 15, c[3], 215);
Draw.rect(ctx, 0, h * 0.9, w, h * 0.1, H.darken(c[0], 0.5));
},
particles: { type: 'fireflies', count: 25, color: '#AAEE44' }
},
desert: {
meta: { id: 'nature_desert', mood: 'warm', colors: ['#F4A460', '#DAA06D', '#C2955A', '#8B6914'], tags: ['sand', 'hot', 'arid'], description: 'Rolling sand dunes under a blazing sun', compatibleMusicMoods: ['calm', 'epic', 'ambient'], recommendedFor: ['racing', 'action', 'rpg'] },
colors: ['#F4A460', '#DAA06D', '#C2955A', '#8B6914'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#FFCC66', '#FF9933', c[0]], 90);
// Sun
var sunPulse = 1 + Math.sin(t * 0.3) * 0.03;
Draw.glow(ctx, w * 0.75, h * 0.12, 100 * sunPulse, H.rgba('#FFEE88', 0.4));
Draw.circle(ctx, w * 0.75, h * 0.12, 35, '#FFDD44');
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Far dunes
Draw.hills(ctx, w, h, h * 0.45, 5, H.lighten(c[0], 0.15), 310);
// Mid dunes
Draw.hills(ctx, w, h, h * 0.6, 4, c[1], 311);
// Near dunes
Draw.hills(ctx, w, h, h * 0.75, 3, c[2], 312);
// Ground
Draw.rect(ctx, 0, h * 0.82, w, h * 0.18, c[3]);
// Heat shimmer
var rng = H.seededRandom(313);
ctx.save();
ctx.globalAlpha = 0.03 + Math.sin(t * 2) * 0.02;
for (var i = 0; i < 8; i++) {
var sy = h * 0.45 + rng() * h * 0.15;
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 1;
ctx.beginPath();
for (var x = 0; x < w; x += 6) {
var yy = sy + Math.sin(x * 0.04 + t * 3 + i) * 3;
x === 0 ? ctx.moveTo(x, yy) : ctx.lineTo(x, yy);
}
ctx.stroke();
}
ctx.restore();
// Cacti
for (var j = 0; j < 4; j++) {
var cx = rng() * w;
var cy = h * 0.72 + rng() * h * 0.06;
var ch = 15 + rng() * 25;
Draw.rect(ctx, cx - 3, cy - ch, 6, ch, '#2D5A27');
Draw.rect(ctx, cx + 3, cy - ch * 0.7, 10, 4, '#2D5A27');
Draw.rect(ctx, cx + 9, cy - ch * 0.7, 4, -10, '#2D5A27');
Draw.rect(ctx, cx - 13, cy - ch * 0.5, 10, 4, '#2D5A27');
Draw.rect(ctx, cx - 13, cy - ch * 0.5, 4, -8, '#2D5A27');
}
},
particles: { type: 'dust', count: 20, color: '#DDCC99' }
},
mountains: {
meta: { id: 'nature_mountains', mood: 'majestic', colors: ['#2C3E50', '#4A6B5A', '#7A9B6B', '#FFFFFF'], tags: ['peaks', 'alpine', 'grand'], description: 'Snow-capped mountain peaks under sweeping skies', compatibleMusicMoods: ['epic', 'calm', 'dreamy'], recommendedFor: ['rpg', 'sports', 'story'] },
colors: ['#2C3E50', '#4A6B5A', '#7A9B6B', '#FFFFFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#6699CC', '#AAC4DD', '#DDEEFF'], 90);
Draw.clouds(ctx, w, h, h * 0.1, 8, '#FFFFFF', 410);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Far mountains
Draw.mountains(ctx, w, h, h * 0.45, 6, H.rgba(c[0], 0.6), 411);
// Snow caps
Draw.mountains(ctx, w, h, h * 0.42, 6, H.rgba(c[3], 0.25), 411);
// Mid mountains
Draw.mountains(ctx, w, h, h * 0.55, 5, c[0], 412);
Draw.mountains(ctx, w, h, h * 0.52, 5, H.rgba(c[3], 0.35), 412);
// Foothills
Draw.hills(ctx, w, h, h * 0.7, 8, c[1], 413);
Draw.hills(ctx, w, h, h * 0.8, 6, c[2], 414);
// Valley floor
Draw.rect(ctx, 0, h * 0.85, w, h * 0.15, H.darken(c[2], 0.2));
// River
ctx.strokeStyle = H.rgba('#5588BB', 0.5);
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(w * 0.3, h * 0.85);
ctx.quadraticCurveTo(w * 0.5, h * 0.88, w * 0.7, h * 0.92);
ctx.quadraticCurveTo(w * 0.85, h * 0.95, w, h * 0.93);
ctx.stroke();
},
particles: { type: 'fog', count: 8, color: '#CCCCCC' }
},
ocean: {
meta: { id: 'nature_ocean', mood: 'serene', colors: ['#1A5276', '#2E86C1', '#5DADE2', '#AED6F1'], tags: ['water', 'waves', 'calm'], description: 'Vast ocean stretching to the horizon at sunset', compatibleMusicMoods: ['calm', 'dreamy', 'ambient'], recommendedFor: ['puzzle', 'story', 'creative'] },
colors: ['#1A5276', '#2E86C1', '#5DADE2', '#AED6F1'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#FF8844', '#FFAA66', '#FFCC88', c[3]], 90);
// Setting sun
var sunY = h * 0.35 + Math.sin(t * 0.1) * 2;
Draw.glow(ctx, w * 0.5, sunY, 120, H.rgba('#FFAA44', 0.4));
Draw.circle(ctx, w * 0.5, sunY, 30, '#FFDD66');
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Horizon line
Draw.rect(ctx, 0, h * 0.45, w, 2, H.rgba('#FFEECC', 0.3));
// Ocean
Draw.water(ctx, w, h, h * 0.45, c[1], 3 + Math.sin(t) * 1, 510);
// Sun reflection on water
var rng = H.seededRandom(511);
ctx.save();
ctx.globalAlpha = 0.08 + Math.sin(t * 0.5) * 0.04;
for (var i = 0; i < 15; i++) {
var ry = h * 0.47 + i * ((h * 0.53) / 15);
var rw = 20 + rng() * 40 - i * 1.5;
var rx = w * 0.5 - rw / 2 + Math.sin(t * 1.5 + i * 0.8) * 10;
Draw.rect(ctx, rx, ry, rw, 2, '#FFDDAA');
}
ctx.restore();
// Distant clouds
Draw.clouds(ctx, w, h, h * 0.15, 4, '#FFDDBB', 512);
},
particles: { type: 'fog', count: 6, color: '#AACCDD' }
},
underwater: {
meta: { id: 'nature_underwater', mood: 'mystical', colors: ['#0A2342', '#1A4B6E', '#2E86C1', '#48D1CC'], tags: ['deep', 'aquatic', 'blue'], description: 'Deep beneath the waves with shafts of sunlight', compatibleMusicMoods: ['dreamy', 'ambient', 'calm'], recommendedFor: ['puzzle', 'pet', 'creative'] },
colors: ['#0A2342', '#1A4B6E', '#2E86C1', '#48D1CC'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[3], c[2], c[1], c[0]], 90);
// Light rays from surface
var rng = H.seededRandom(610);
ctx.save();
for (var i = 0; i < 6; i++) {
var rx = rng() * w;
var sway = Math.sin(t * 0.4 + i * 1.5) * 15;
ctx.fillStyle = H.rgba('#AADDFF', 0.03 + Math.sin(t * 0.3 + i) * 0.01);
ctx.beginPath();
ctx.moveTo(rx + sway, 0);
ctx.lineTo(rx - 40 + sway * 0.5, h);
ctx.lineTo(rx + 40 + sway * 0.5, h);
ctx.closePath();
ctx.fill();
}
ctx.restore();
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(611);
// Seaweed
for (var i = 0; i < 10; i++) {
var sx = rng() * w;
var sh = 40 + rng() * 80;
var sway = Math.sin(t * 0.8 + i * 2) * 10;
ctx.strokeStyle = H.rgba('#228844', 0.5 + rng() * 0.3);
ctx.lineWidth = 3 + rng() * 3;
ctx.beginPath();
ctx.moveTo(sx, h);
ctx.quadraticCurveTo(sx + sway, h - sh * 0.5, sx + sway * 1.3, h - sh);
ctx.stroke();
}
// Coral mounds
for (var j = 0; j < 6; j++) {
var cx = rng() * w;
var cr = 15 + rng() * 30;
var cc = H.pick(['#CC4466', '#FF7744', '#FFAA33', '#AA55CC']);
Draw.circle(ctx, cx, h - cr * 0.5, cr, H.rgba(cc, 0.5));
Draw.circle(ctx, cx + cr * 0.4, h - cr * 0.3, cr * 0.6, H.rgba(cc, 0.35));
}
// Fish silhouettes
for (var k = 0; k < 8; k++) {
var fx = (rng() * w + t * (10 + rng() * 30)) % (w + 60) - 30;
var fy = h * 0.2 + rng() * h * 0.5;
var fs = 4 + rng() * 8;
ctx.fillStyle = H.rgba(c[3], 0.3 + rng() * 0.2);
ctx.beginPath();
ctx.ellipse(fx, fy, fs * 2, fs, 0, 0, Math.PI * 2);
ctx.fill();
// Tail
ctx.beginPath();
ctx.moveTo(fx - fs * 2, fy);
ctx.lineTo(fx - fs * 3, fy - fs * 0.8);
ctx.lineTo(fx - fs * 3, fy + fs * 0.8);
ctx.closePath();
ctx.fill();
}
// Sandy bottom
Draw.rect(ctx, 0, h * 0.92, w, h * 0.08, H.rgba('#C2B280', 0.25));
},
particles: { type: 'bubbles', count: 30, color: '#88CCEE' }
},
waterfall: {
meta: { id: 'nature_waterfall', mood: 'refreshing', colors: ['#1B4332', '#2D6A4F', '#52B788', '#B7E4C7'], tags: ['water', 'lush', 'dynamic'], description: 'A cascading waterfall in a lush green cliff', compatibleMusicMoods: ['calm', 'ambient', 'dreamy'], recommendedFor: ['puzzle', 'creative', 'story'] },
colors: ['#1B4332', '#2D6A4F', '#52B788', '#B7E4C7'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#77AABB', '#99CCDD', c[2]], 90);
Draw.clouds(ctx, w, h, h * 0.05, 3, '#FFFFFF', 710);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Cliff walls
Draw.rect(ctx, 0, h * 0.15, w * 0.35, h * 0.85, c[0]);
Draw.rect(ctx, w * 0.65, h * 0.15, w * 0.35, h * 0.85, c[0]);
// Cliff vegetation
Draw.hills(ctx, w * 0.35, h * 0.2, h * 0.15, 5, c[1], 711);
ctx.save(); ctx.translate(w * 0.65, 0);
Draw.hills(ctx, w * 0.35, h * 0.2, h * 0.15, 5, c[1], 712);
ctx.restore();
// Waterfall
var rng = H.seededRandom(713);
var fallX = w * 0.38, fallW = w * 0.24;
for (var i = 0; i < 20; i++) {
var fx = fallX + rng() * fallW;
var fy = h * 0.15 + rng() * h * 0.65;
var offset = (t * 80 + rng() * 200) % (h * 0.7);
var wy = h * 0.15 + offset;
ctx.strokeStyle = H.rgba('#FFFFFF', 0.1 + rng() * 0.2);
ctx.lineWidth = 1 + rng() * 2;
ctx.beginPath();
ctx.moveTo(fx, wy);
ctx.lineTo(fx + (rng() - 0.5) * 4, wy + 10 + rng() * 20);
ctx.stroke();
}
// Base glow
Draw.fillGradient(ctx, w, h, [H.rgba(c[3], 0), H.rgba(c[3], 0), H.rgba('#FFFFFF', 0.15)], 90);
// Pool at bottom
Draw.water(ctx, w, h, h * 0.82, H.rgba('#3388AA', 0.6), 2, 714);
// Mist
Draw.glow(ctx, w * 0.5, h * 0.8, 100, H.rgba('#FFFFFF', 0.12 + Math.sin(t) * 0.04));
// Foreground plants
Draw.trees(ctx, w, h, h * 0.9, 6, '#3D2B1F', c[2], 715);
},
particles: { type: 'fog', count: 12, color: '#DDEEFF' }
},
meadow: {
meta: { id: 'nature_meadow', mood: 'joyful', colors: ['#4CAF50', '#81C784', '#A5D6A7', '#FFF59D'], tags: ['flowers', 'sunny', 'cheerful'], description: 'A sunny meadow with wildflowers and gentle breeze', compatibleMusicMoods: ['happy', 'calm', 'dreamy'], recommendedFor: ['pet', 'creative', 'story', 'puzzle'] },
colors: ['#4CAF50', '#81C784', '#A5D6A7', '#FFF59D'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#55AADD', '#88CCEE', '#AADDEE'], 90);
// Sun
var pulse = 1 + Math.sin(t * 0.4) * 0.02;
Draw.glow(ctx, w * 0.8, h * 0.1, 80 * pulse, H.rgba('#FFEE66', 0.4));
Draw.circle(ctx, w * 0.8, h * 0.1, 28, '#FFEE44');
Draw.clouds(ctx, w, h, h * 0.08, 6, '#FFFFFF', 810);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Distant hills
Draw.hills(ctx, w, h, h * 0.55, 5, H.darken(c[0], 0.15), 811);
// Main meadow
Draw.hills(ctx, w, h, h * 0.65, 8, c[0], 812);
Draw.hills(ctx, w, h, h * 0.72, 6, c[1], 813);
// Ground
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, c[2]);
// Flowers
var rng = H.seededRandom(814);
var flowerColors = ['#FF6B6B', '#FFD93D', '#C084FC', '#FF8FA3', '#FFFFFF'];
for (var i = 0; i < 40; i++) {
var fx = rng() * w;
var fy = h * 0.68 + rng() * h * 0.25;
var fc = flowerColors[Math.floor(rng() * flowerColors.length)];
var sway = Math.sin(t * 1.2 + i * 0.8) * 2;
// Stem
ctx.strokeStyle = H.rgba('#338833', 0.5);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(fx, fy + 8);
ctx.lineTo(fx + sway, fy);
ctx.stroke();
// Flower
Draw.circle(ctx, fx + sway, fy, 2 + rng() * 2, fc);
}
// Butterflies
for (var j = 0; j < 3; j++) {
var bx = (rng() * w + Math.sin(t * 0.5 + j * 3) * 40);
var by = h * 0.4 + rng() * h * 0.3 + Math.sin(t * 1.2 + j * 2) * 15;
var wingFlap = Math.abs(Math.sin(t * 5 + j * 2)) * 4;
var bc = H.pick(['#FFD700', '#FF69B4', '#87CEEB']);
ctx.fillStyle = H.rgba(bc, 0.7);
ctx.beginPath();
ctx.ellipse(bx - 3, by, wingFlap, 3, -0.3, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(bx + 3, by, wingFlap, 3, 0.3, 0, Math.PI * 2);
ctx.fill();
Draw.circle(ctx, bx, by, 1, '#333333');
}
},
particles: { type: 'leaves', count: 10, color: '#AEDD77' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,551 @@
// retro.js — Retro theme backgrounds
// Variants: pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('retro', {
pixelGrid: {
meta: { id: 'retro_pixelGrid', mood: 'nostalgic', colors: ['#1a1a2e', '#16213e', '#00ff88', '#ff6600'], tags: ['pixel', 'grid', 'retro', 'classic'], description: 'A chunky pixel grid with classic 8-bit color blocks and pulsing cursor', compatibleMusicMoods: ['retro', 'playful', 'electronic'], recommendedFor: ['action', 'puzzle', 'creative'] },
colors: ['#1a1a2e', '#16213e', '#00ff88', '#ff6600'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.grid(ctx, w, h, 32, H.rgba(c[2], 0.04), 1);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(100);
var pixelSize = 32;
var cols = Math.ceil(w / pixelSize);
var rows = Math.ceil(h / pixelSize);
// Scattered colored pixels
for (var i = 0; i < 30; i++) {
var px = Math.floor(rng() * cols) * pixelSize;
var py = Math.floor(rng() * rows) * pixelSize;
var pColor = rng() > 0.5 ? c[2] : c[3];
var pAlpha = 0.06 + rng() * 0.1 + Math.sin(t * 0.8 + i * 0.5) * 0.03;
Draw.rect(ctx, px, py, pixelSize - 1, pixelSize - 1, H.rgba(pColor, pAlpha));
}
// Pixel art sprite (simple ghost/invader shape)
var spriteX = w * 0.5, spriteY = h * 0.4;
var sp = pixelSize * 0.5;
var spriteMap = [
[0,0,1,1,1,0,0],
[0,1,1,1,1,1,0],
[1,1,0,1,0,1,1],
[1,1,1,1,1,1,1],
[1,0,1,0,1,0,1],
[0,1,0,1,0,1,0]
];
var spriteAlpha = 0.15 + Math.sin(t * 0.6) * 0.05;
var sOffX = spriteX - (spriteMap[0].length * sp) / 2;
var sOffY = spriteY - (spriteMap.length * sp) / 2;
for (var sr = 0; sr < spriteMap.length; sr++) {
for (var sc = 0; sc < spriteMap[sr].length; sc++) {
if (spriteMap[sr][sc]) {
Draw.rect(ctx, sOffX + sc * sp, sOffY + sr * sp, sp - 1, sp - 1, H.rgba(c[2], spriteAlpha));
}
}
}
// Blinking cursor
var cursorOn = Math.sin(t * 3) > 0;
if (cursorOn) {
Draw.rect(ctx, w * 0.3, h * 0.75, pixelSize * 0.6, pixelSize, H.rgba(c[2], 0.4));
}
// Score text (pixel-styled line)
for (var sc2 = 0; sc2 < 6; sc2++) {
Draw.rect(ctx, w * 0.35 + sc2 * 12, h * 0.12, 8, 3, H.rgba(c[3], 0.12));
}
},
particles: { type: 'sparkles', count: 10, color: '#00FF88' }
},
arcadeCabinet: {
meta: { id: 'retro_arcadeCabinet', mood: 'fun', colors: ['#0a0a15', '#1a0a2e', '#ff2277', '#ffcc00'], tags: ['arcade', 'cabinet', 'neon', 'classic'], description: 'The inside view of a dark arcade with neon-lit cabinet edges and attract mode glow', compatibleMusicMoods: ['retro', 'energetic', 'playful'], recommendedFor: ['action', 'racing', 'sports'] },
colors: ['#0a0a15', '#1a0a2e', '#ff2277', '#ffcc00'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Ambient arcade glow
Draw.glow(ctx, w * 0.5, h * 0.5, w * 0.5, H.rgba(c[2], 0.04));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(200);
// Floor tiles (perspective grid)
ctx.strokeStyle = H.rgba(c[2], 0.05);
ctx.lineWidth = 1;
for (var fl = 0; fl < 15; fl++) {
var fy = h * 0.7 + fl * h * 0.02;
ctx.beginPath(); ctx.moveTo(0, fy); ctx.lineTo(w, fy); ctx.stroke();
}
for (var fv = 0; fv < 12; fv++) {
var fx = fv * w / 12;
ctx.beginPath(); ctx.moveTo(fx, h * 0.7); ctx.lineTo(w * 0.5 + (fx - w * 0.5) * 0.3, h); ctx.stroke();
}
// Arcade cabinets (silhouettes with neon trim)
var cabinets = [
{ x: 0.12, scale: 0.9 }, { x: 0.32, scale: 1.0 },
{ x: 0.52, scale: 1.0 }, { x: 0.72, scale: 0.9 }, { x: 0.88, scale: 0.8 }
];
for (var cab = 0; cab < cabinets.length; cab++) {
var cbx = cabinets[cab].x * w;
var cbs = cabinets[cab].scale;
var cbw = w * 0.12 * cbs;
var cbh = h * 0.45 * cbs;
var cby = h * 0.68 - cbh;
// Cabinet body
Draw.rect(ctx, cbx - cbw / 2, cby, cbw, cbh, '#0a0a12');
// Screen area
var scrW = cbw * 0.7, scrH = cbh * 0.4;
var scrX = cbx - scrW / 2, scrY = cby + cbh * 0.1;
var screenColor = rng() > 0.5 ? c[2] : c[3];
var screenGlow = 0.15 + Math.sin(t * 1.5 + cab * 1.2) * 0.08;
Draw.rect(ctx, scrX, scrY, scrW, scrH, H.rgba(screenColor, screenGlow));
Draw.glow(ctx, cbx, scrY + scrH / 2, scrW * 0.6, H.rgba(screenColor, screenGlow * 0.3));
// Neon trim
ctx.strokeStyle = H.rgba(screenColor, 0.25 + Math.sin(t * 2 + cab) * 0.1);
ctx.lineWidth = 1.5;
ctx.strokeRect(scrX - 2, scrY - 2, scrW + 4, scrH + 4);
// Control panel
Draw.rect(ctx, cbx - cbw * 0.4, cby + cbh * 0.7, cbw * 0.8, cbh * 0.1, '#151520');
// Joystick and buttons
Draw.circle(ctx, cbx - cbw * 0.15, cby + cbh * 0.75, 3, '#333');
for (var btn = 0; btn < 3; btn++) {
Draw.circle(ctx, cbx + cbw * 0.05 + btn * 10, cby + cbh * 0.75, 3,
H.rgba(btn === 0 ? '#FF2277' : btn === 1 ? '#2277FF' : '#FFCC00', 0.4));
}
}
// "INSERT COIN" text glow
var coinBlink = Math.sin(t * 2) > 0;
if (coinBlink) {
ctx.font = 'bold 14px monospace';
ctx.fillStyle = H.rgba(c[3], 0.3);
ctx.textAlign = 'center';
ctx.fillText('INSERT COIN', w * 0.5, h * 0.88);
ctx.textAlign = 'start';
}
// Neon sign at top
ctx.strokeStyle = H.rgba(c[2], 0.2 + Math.sin(t * 1.8) * 0.08);
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(w * 0.25, h * 0.08); ctx.lineTo(w * 0.75, h * 0.08); ctx.stroke();
Draw.glow(ctx, w * 0.5, h * 0.08, w * 0.3, H.rgba(c[2], 0.04));
},
particles: { type: 'dust', count: 15, color: '#FF2277' }
},
crtEffects: {
meta: { id: 'retro_crtEffects', mood: 'vintage', colors: ['#0a0a0a', '#0f1a0f', '#33ff33', '#88ff88'], tags: ['crt', 'monitor', 'phosphor', 'vintage'], description: 'Authentic CRT monitor with phosphor glow, barrel distortion and scan line interference', compatibleMusicMoods: ['retro', 'dark', 'electronic'], recommendedFor: ['action', 'puzzle', 'trivia'] },
colors: ['#0a0a0a', '#0f1a0f', '#33ff33', '#88ff88'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Phosphor glow in center
Draw.glow(ctx, w * 0.5, h * 0.5, Math.min(w, h) * 0.5, H.rgba(c[2], 0.03));
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Scan lines
Draw.scanlines(ctx, w, h, 3, H.rgba(c[2], 0.03 + Math.sin(t * 0.5) * 0.01));
// RGB sub-pixel columns
for (var col = 0; col < w; col += 3) {
Draw.rect(ctx, col, 0, 1, h, H.rgba('#FF0000', 0.008));
Draw.rect(ctx, col + 1, 0, 1, h, H.rgba('#00FF00', 0.008));
Draw.rect(ctx, col + 2, 0, 1, h, H.rgba('#0000FF', 0.008));
}
// Rolling interference band
var bandY = ((t * 30) % (h + 80)) - 40;
ctx.fillStyle = H.rgba(c[2], 0.04);
ctx.fillRect(0, bandY, w, 40);
ctx.fillStyle = H.rgba(c[2], 0.06);
ctx.fillRect(0, bandY + 15, w, 8);
// Static noise bursts
var rng = H.seededRandom(Math.floor(t * 5) + 300);
if (rng() > 0.7) {
for (var nz = 0; nz < 50; nz++) {
var nx = rng() * w, ny = rng() * h;
var nw = 1 + rng() * 4;
Draw.rect(ctx, nx, ny, nw, 1, H.rgba(c[2], rng() * 0.15));
}
}
// Faux text content
var txtrng = H.seededRandom(301);
ctx.font = '12px monospace';
for (var ln = 0; ln < 8; ln++) {
var lineY = h * 0.25 + ln * 22;
var lineStr = '';
var lineLen = 10 + Math.floor(txtrng() * 25);
for (var ch = 0; ch < lineLen; ch++) {
lineStr += String.fromCharCode(33 + Math.floor(txtrng() * 93));
}
var txtAlpha = 0.08 + Math.sin(t * 0.4 + ln * 0.5) * 0.03;
ctx.fillStyle = H.rgba(c[2], txtAlpha);
ctx.fillText(lineStr, w * 0.15, lineY);
}
// Prompt cursor
var cursorBlink = Math.sin(t * 3) > 0;
if (cursorBlink) {
Draw.rect(ctx, w * 0.15, h * 0.25 + 8 * 22, 8, 14, H.rgba(c[2], 0.3));
}
// Vignette / barrel distortion feel
var vig = Draw.radialGradient(ctx, w / 2, h / 2, Math.max(w, h) * 0.65, [
[0, H.rgba(c[0], 0)],
[0.6, H.rgba(c[0], 0)],
[1, H.rgba(c[0], 0.7)]
]);
ctx.fillStyle = vig;
ctx.fillRect(0, 0, w, h);
// CRT bezel border
ctx.strokeStyle = H.rgba('#333333', 0.3);
ctx.lineWidth = 8;
ctx.strokeRect(4, 4, w - 8, h - 8);
},
particles: null
},
vaporwave: {
meta: { id: 'retro_vaporwave', mood: 'aesthetic', colors: ['#0d0221', '#261447', '#ff71ce', '#01cdfe'], tags: ['vaporwave', 'aesthetic', '90s', 'neon'], description: 'A e s t h e t i c sunset horizon with a perspective grid, palm silhouettes and neon glow', compatibleMusicMoods: ['dreamy', 'retro', 'calm'], recommendedFor: ['creative', 'music', 'racing'] },
colors: ['#0d0221', '#261447', '#ff71ce', '#01cdfe'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Sunset gradient
Draw.fillGradient(ctx, w, h, [c[0], c[1], '#ff71ce', '#ff9de2'], 90);
// Sun
var sunY = h * 0.42 + Math.sin(t * 0.2) * 3;
var sunGrad = Draw.linearGradient(ctx, 0, sunY - 60, 0, sunY + 60, [
[0, '#FF6B9D'], [0.5, '#FFBE0B'], [1, '#FF006E']
]);
ctx.fillStyle = sunGrad;
ctx.beginPath(); ctx.arc(w * 0.5, sunY, 60, 0, Math.PI * 2); ctx.fill();
// Sun horizontal stripes
for (var ss = 0; ss < 6; ss++) {
var ssy = sunY + ss * 10 - 10;
var ssH = 3 + ss * 0.8;
Draw.rect(ctx, w * 0.5 - 60, ssy, 120, ssH, c[0]);
}
Draw.glow(ctx, w * 0.5, sunY, 120, H.rgba('#FF71CE', 0.15));
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Horizon line
var horizonY = h * 0.55;
Draw.rect(ctx, 0, horizonY, w, h - horizonY, '#0d0221');
// Perspective grid on ground plane
var gridColor = H.rgba(c[2], 0.2);
ctx.strokeStyle = gridColor;
ctx.lineWidth = 1;
// Horizontal lines (receding)
for (var gy = 0; gy < 20; gy++) {
var lineFrac = gy / 20;
var lineY = horizonY + lineFrac * lineFrac * (h - horizonY);
ctx.beginPath(); ctx.moveTo(0, lineY); ctx.lineTo(w, lineY); ctx.stroke();
}
// Vertical lines (converging at center horizon)
for (var gx = -8; gx <= 8; gx++) {
var topX = w * 0.5 + gx * 2;
var botX = w * 0.5 + gx * w * 0.08;
ctx.strokeStyle = H.rgba(c[3], 0.15);
ctx.beginPath(); ctx.moveTo(topX, horizonY); ctx.lineTo(botX, h); ctx.stroke();
}
// Palm tree silhouettes
var rng = H.seededRandom(400);
var palms = [{ x: 0.12, s: 1.0 }, { x: 0.85, s: 0.9 }, { x: 0.05, s: 0.7 }];
for (var p = 0; p < palms.length; p++) {
var px = palms[p].x * w;
var ps = palms[p].s;
var trunkH = h * 0.35 * ps;
var palmBot = horizonY;
// Trunk
ctx.strokeStyle = '#0d0221';
ctx.lineWidth = 4 * ps;
ctx.beginPath();
ctx.moveTo(px, palmBot);
ctx.quadraticCurveTo(px + 10 * ps, palmBot - trunkH * 0.6, px + 5 * ps, palmBot - trunkH);
ctx.stroke();
// Fronds
for (var fr = 0; fr < 6; fr++) {
var frAngle = (fr / 6) * Math.PI * 2;
var frLen = 30 * ps + Math.sin(t * 0.8 + fr) * 3;
var sway = Math.sin(t * 0.5 + fr * 0.3) * 5;
ctx.strokeStyle = '#0d0221';
ctx.lineWidth = 2 * ps;
ctx.beginPath();
ctx.moveTo(px + 5 * ps, palmBot - trunkH);
ctx.quadraticCurveTo(
px + 5 * ps + Math.cos(frAngle) * frLen * 0.5 + sway,
palmBot - trunkH + Math.sin(frAngle) * frLen * 0.3 - 10,
px + 5 * ps + Math.cos(frAngle) * frLen + sway,
palmBot - trunkH + Math.sin(frAngle) * frLen * 0.5 + 5
);
ctx.stroke();
}
}
// Neon highlights on horizon
ctx.strokeStyle = H.rgba(c[3], 0.3 + Math.sin(t) * 0.1);
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, horizonY); ctx.lineTo(w, horizonY); ctx.stroke();
},
particles: { type: 'stars', count: 40, color: '#FF71CE' }
},
scanlines: {
meta: { id: 'retro_scanlines', mood: 'technical', colors: ['#0a0a1a', '#141428', '#00aaff', '#ff4444'], tags: ['scanlines', 'display', 'technical', 'monitor'], description: 'Dense horizontal scan lines over a shifting color field with signal noise artifacts', compatibleMusicMoods: ['electronic', 'tense', 'dark'], recommendedFor: ['action', 'puzzle', 'physics'] },
colors: ['#0a0a1a', '#141428', '#00aaff', '#ff4444'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Color field blocks shifting
var rng = H.seededRandom(500);
for (var blk = 0; blk < 8; blk++) {
var bx = rng() * w * 0.8;
var by = rng() * h * 0.8;
var bw = w * 0.1 + rng() * w * 0.2;
var bh = h * 0.05 + rng() * h * 0.15;
var bColor = rng() > 0.5 ? c[2] : c[3];
var bAlpha = 0.03 + Math.sin(t * 0.3 + blk * 0.7) * 0.02;
Draw.rect(ctx, bx, by, bw, bh, H.rgba(bColor, bAlpha));
}
// Dense scanlines
Draw.scanlines(ctx, w, h, 2, H.rgba('#FFFFFF', 0.03));
// Thicker periodic scanlines
for (var tsl = 0; tsl < h; tsl += 16) {
Draw.rect(ctx, 0, tsl, w, 1, H.rgba(c[2], 0.02 + Math.sin(t * 0.5 + tsl * 0.01) * 0.01));
}
// Horizontal displacement glitch bands
var glitchRng = H.seededRandom(Math.floor(t * 2) + 501);
if (glitchRng() > 0.6) {
for (var gb = 0; gb < 3; gb++) {
var gy = glitchRng() * h;
var gHeight = 2 + glitchRng() * 8;
var gOffset = (glitchRng() - 0.5) * 20;
ctx.fillStyle = H.rgba(c[2], 0.08);
ctx.fillRect(gOffset, gy, w, gHeight);
ctx.fillStyle = H.rgba(c[3], 0.06);
ctx.fillRect(-gOffset, gy + 1, w, gHeight * 0.5);
}
}
// Signal meter bars
for (var bar = 0; bar < 12; bar++) {
var barH = h * 0.03 + Math.sin(t * 2 + bar * 0.5) * h * 0.02;
var barColor = bar < 8 ? c[2] : c[3];
Draw.rect(ctx, w * 0.05 + bar * w * 0.04, h * 0.9 - barH, w * 0.03, barH, H.rgba(barColor, 0.15));
}
// Crosshair / target
var chAlpha = 0.1 + Math.sin(t * 0.6) * 0.04;
ctx.strokeStyle = H.rgba(c[2], chAlpha);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(w * 0.5, h * 0.5, 30, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w * 0.5 - 40, h * 0.5); ctx.lineTo(w * 0.5 + 40, h * 0.5); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w * 0.5, h * 0.5 - 40); ctx.lineTo(w * 0.5, h * 0.5 + 40); ctx.stroke();
},
particles: null
},
glitch: {
meta: { id: 'retro_glitch', mood: 'chaotic', colors: ['#0a0a0a', '#1a0a1a', '#ff0055', '#00ffcc'], tags: ['glitch', 'broken', 'chaotic', 'digital'], description: 'Fragmented display with chromatic aberration, displaced blocks and corrupted data streaks', compatibleMusicMoods: ['chaotic', 'electronic', 'tense'], recommendedFor: ['action', 'racing', 'physics'] },
colors: ['#0a0a0a', '#1a0a1a', '#ff0055', '#00ffcc'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(Math.floor(t * 3) + 600);
// Displaced rectangular blocks
for (var blk = 0; blk < 6; blk++) {
var bx = rng() * w;
var by = rng() * h;
var bw = 20 + rng() * 100;
var bh = 3 + rng() * 20;
var shift = (rng() - 0.5) * 30;
// RGB channel separation
Draw.rect(ctx, bx + shift, by, bw, bh, H.rgba(c[2], 0.08 + rng() * 0.1));
Draw.rect(ctx, bx - shift, by + 1, bw, bh, H.rgba(c[3], 0.06 + rng() * 0.08));
}
// Static deterministic elements
var srng = H.seededRandom(601);
// Corrupted horizontal streaks
for (var str = 0; str < 15; str++) {
var sy = srng() * h;
var sx = srng() * w * 0.3;
var sw = w * 0.2 + srng() * w * 0.5;
var jitter = Math.sin(t * 4 + str * 2) > 0.7 ? (srng() - 0.5) * 10 : 0;
Draw.rect(ctx, sx + jitter, sy, sw, 1, H.rgba(srng() > 0.5 ? c[2] : c[3], 0.06));
}
// Large chromatic aberration text
ctx.font = 'bold 48px monospace';
var errAlpha = 0.06 + Math.sin(t * 1.5) * 0.03;
var errShift = 3 + Math.sin(t * 5) * 2;
ctx.fillStyle = H.rgba(c[2], errAlpha);
ctx.fillText('ERR', w * 0.35 - errShift, h * 0.5);
ctx.fillStyle = H.rgba(c[3], errAlpha);
ctx.fillText('ERR', w * 0.35 + errShift, h * 0.5 + 2);
ctx.fillStyle = H.rgba('#FFFFFF', errAlpha * 0.5);
ctx.fillText('ERR', w * 0.35, h * 0.5 + 1);
// Data corruption blocks
for (var dc = 0; dc < 10; dc++) {
var dcx = srng() * w, dcy = srng() * h;
var dcw = 4 + srng() * 12, dch = 4 + srng() * 12;
var dcPulse = Math.sin(t * 3 + dc * 1.1) > 0.5;
if (dcPulse) {
Draw.rect(ctx, dcx, dcy, dcw, dch, H.rgba(srng() > 0.5 ? c[2] : c[3], 0.15));
}
}
// Thin scan lines
Draw.scanlines(ctx, w, h, 4, H.rgba('#FFFFFF', 0.02));
// Occasional full-width flash
if (Math.sin(t * 0.8) > 0.97) {
Draw.rect(ctx, 0, 0, w, h, H.rgba('#FFFFFF', 0.03));
}
},
particles: { type: 'particles', count: 15, color: '#FF0055' }
},
eightBit: {
meta: { id: 'retro_eightBit', mood: 'cheerful', colors: ['#2b2b6e', '#3d3d94', '#5bc0eb', '#fde74c'], tags: ['8bit', 'nintendo', 'cheerful', 'classic'], description: 'A charming 8-bit scene with blocky pixel clouds, hills and a bright primary-color sky', compatibleMusicMoods: ['playful', 'retro', 'cheerful'], recommendedFor: ['action', 'puzzle', 'creative'] },
colors: ['#2b2b6e', '#3d3d94', '#5bc0eb', '#fde74c'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Bright 8-bit sky
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(700);
var px = 8; // pixel size for blocky look
// Pixel sun
var sunX = w * 0.75, sunY = h * 0.2;
var sunPx = [
[-1,-2],[0,-2],[1,-2],
[-2,-1],[-1,-1],[0,-1],[1,-1],[2,-1],
[-2,0],[-1,0],[0,0],[1,0],[2,0],
[-2,1],[-1,1],[0,1],[1,1],[2,1],
[-1,2],[0,2],[1,2]
];
for (var sp = 0; sp < sunPx.length; sp++) {
Draw.rect(ctx, sunX + sunPx[sp][0] * px, sunY + sunPx[sp][1] * px, px, px, c[3]);
}
// Pixel clouds
var cloudPositions = [{ x: 0.15, y: 0.18 }, { x: 0.45, y: 0.12 }, { x: 0.8, y: 0.22 }];
var cloudShape = [[0,0],[1,0],[2,0],[3,0],[-1,1],[0,1],[1,1],[2,1],[3,1],[4,1],[0,2],[1,2],[2,2],[3,2]];
for (var cl = 0; cl < cloudPositions.length; cl++) {
var clx = cloudPositions[cl].x * w + Math.sin(t * 0.3 + cl) * 5;
var cly = cloudPositions[cl].y * h;
for (var cp = 0; cp < cloudShape.length; cp++) {
Draw.rect(ctx, clx + cloudShape[cp][0] * px, cly + cloudShape[cp][1] * px, px, px, '#FFFFFF');
}
}
// Pixel hills (green terrain at bottom)
var groundY = h * 0.7;
Draw.rect(ctx, 0, groundY, w, h - groundY, '#4CAF50');
// Stepped hill shapes
var hillRng = H.seededRandom(701);
for (var hill = 0; hill < 5; hill++) {
var hx = hillRng() * w;
var hPeakH = 4 + Math.floor(hillRng() * 6);
for (var hr = 0; hr < hPeakH; hr++) {
var hRowW = (hPeakH - hr) * 2 + 1;
for (var hc = 0; hc < hRowW; hc++) {
Draw.rect(ctx, hx + (hc - Math.floor(hRowW / 2)) * px, groundY - hr * px, px, px, '#388E3C');
}
}
}
// Ground detail
for (var gd = 0; gd < 15; gd++) {
var gdx = Math.floor(rng() * (w / px)) * px;
var gdy = groundY + Math.floor(rng() * ((h - groundY) / px)) * px;
Draw.rect(ctx, gdx, gdy, px, px, H.rgba('#2E7D32', 0.5));
}
// Small pixel flowers
var flowerColors = ['#FF5252', '#FFD740', '#E040FB', '#FFFFFF'];
for (var fl = 0; fl < 8; fl++) {
var flx = Math.floor(rng() * (w / px)) * px;
var fly = groundY + px + Math.floor(rng() * 3) * px;
Draw.rect(ctx, flx, fly, px, px, flowerColors[fl % flowerColors.length]);
}
// Coin block (floating, animated)
var coinY = h * 0.5 + Math.sin(t * 2) * 2;
var blockPulse = Math.sin(t * 1.5) > 0.8 ? '#FFD740' : c[3];
Draw.rect(ctx, w * 0.5, coinY, px * 2, px * 2, blockPulse);
ctx.strokeStyle = H.rgba('#000', 0.3);
ctx.lineWidth = 1;
ctx.strokeRect(w * 0.5, coinY, px * 2, px * 2);
Draw.rect(ctx, w * 0.5 + 3, coinY + 3, px * 2 - 6, px * 2 - 6, H.darken(blockPulse, 0.2));
},
particles: null
},
synthwave: {
meta: { id: 'retro_synthwave', mood: 'epic', colors: ['#0d0015', '#1a0033', '#ff2975', '#f222ff'], tags: ['synthwave', 'outrun', 'neon', 'night'], description: 'An outrun-style neon horizon with a receding road, mountain silhouettes and electric sky', compatibleMusicMoods: ['epic', 'energetic', 'electronic'], recommendedFor: ['racing', 'action', 'sports'] },
colors: ['#0d0015', '#1a0033', '#ff2975', '#f222ff'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Dark sky with neon gradient at horizon
var skyGrad = Draw.linearGradient(ctx, 0, 0, 0, h * 0.55, [
[0, c[0]], [0.5, c[1]], [0.85, '#330044'], [1, '#ff297530']
]);
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, w, h * 0.55);
Draw.stars(ctx, w, h * 0.4, 70, '#FFFFFF', 801, [0.5, 1.5]);
// Neon sun (half visible at horizon)
var sunCy = h * 0.53;
var sunGrad = Draw.linearGradient(ctx, 0, sunCy - 50, 0, sunCy + 50, [
[0, c[2]], [0.5, c[3]], [1, '#FF6600']
]);
ctx.fillStyle = sunGrad;
ctx.beginPath(); ctx.arc(w * 0.5, sunCy, 55, Math.PI, 0); ctx.fill();
// Sun horizontal lines
for (var sl = 0; sl < 5; sl++) {
var sly = sunCy - 45 + sl * 12;
Draw.rect(ctx, w * 0.5 - 55, sly, 110, 3 + sl, c[0]);
}
Draw.glow(ctx, w * 0.5, sunCy, 100, H.rgba(c[2], 0.1));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var horizonY = h * 0.55;
// Mountain silhouettes
Draw.mountains(ctx, w, h, horizonY, 5, '#0a000f', 802);
// Ground plane
Draw.rect(ctx, 0, horizonY, w, h - horizonY, '#05000a');
// Perspective grid - horizontal lines (receding, animated)
var gridOffset = (t * 20) % 30;
ctx.strokeStyle = H.rgba(c[3], 0.25);
ctx.lineWidth = 1;
for (var gy = 0; gy < 25; gy++) {
var gridFrac = (gy + gridOffset / 30) / 25;
var lineY = horizonY + gridFrac * gridFrac * (h - horizonY);
var lineAlpha = gridFrac * 0.3;
ctx.strokeStyle = H.rgba(c[3], lineAlpha);
ctx.beginPath(); ctx.moveTo(0, lineY); ctx.lineTo(w, lineY); ctx.stroke();
}
// Vertical converging lines
ctx.strokeStyle = H.rgba(c[2], 0.2);
for (var gx = -10; gx <= 10; gx++) {
var topX = w * 0.5;
var botX = w * 0.5 + gx * w * 0.065;
ctx.beginPath(); ctx.moveTo(topX, horizonY); ctx.lineTo(botX, h); ctx.stroke();
}
// Road center stripe
for (var rs = 0; rs < 12; rs++) {
var rsFrac = rs / 12;
var rsY = horizonY + rsFrac * rsFrac * (h - horizonY);
var rsW = 2 + rsFrac * 6;
var rsH = 3 + rsFrac * 8;
var rsAlpha = 0.3 + rsFrac * 0.4;
Draw.rect(ctx, w * 0.5 - rsW / 2, rsY, rsW, rsH, H.rgba('#FFFFFF', rsAlpha));
}
// Neon side lights along road
for (var nl = 0; nl < 6; nl++) {
var nlFrac = (nl + 0.5) / 6;
var nlY = horizonY + nlFrac * nlFrac * (h - horizonY);
var spread = nlFrac * w * 0.3;
var nlSize = 2 + nlFrac * 5;
var nlPulse = 0.4 + Math.sin(t * 2 + nl) * 0.2;
// Left
Draw.glow(ctx, w * 0.5 - spread, nlY, nlSize * 3, H.rgba(c[2], nlPulse * 0.3));
Draw.circle(ctx, w * 0.5 - spread, nlY, nlSize, H.rgba(c[2], nlPulse));
// Right
Draw.glow(ctx, w * 0.5 + spread, nlY, nlSize * 3, H.rgba(c[3], nlPulse * 0.3));
Draw.circle(ctx, w * 0.5 + spread, nlY, nlSize, H.rgba(c[3], nlPulse));
}
// Horizon glow line
ctx.strokeStyle = H.rgba(c[2], 0.35 + Math.sin(t * 0.8) * 0.1);
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, horizonY); ctx.lineTo(w, horizonY); ctx.stroke();
},
particles: { type: 'stars', count: 20, color: '#F222FF' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,385 @@
// seasonal.js — Seasonal background theme (8 variants)
// Depends on backgroundEngine.js being loaded first
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('seasonal', {
springMeadow: {
meta: { id: 'seasonal_springMeadow', mood: 'cheerful', colors: ['#87CEEB', '#98FB98', '#FFD700', '#FF69B4'], tags: ['spring', 'flowers', 'meadow', 'bright'], description: 'A sunny spring meadow with wildflowers and gentle hills', compatibleMusicMoods: ['happy', 'upbeat', 'peaceful'], recommendedFor: ['puzzle', 'creative', 'pet'] },
colors: ['#87CEEB', '#98FB98', '#FFD700', '#FF69B4'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], H.lighten(c[0], 0.3)], 90);
Draw.hills(ctx, w, h, h * 0.6, 6, c[1], 10);
Draw.hills(ctx, w, h, h * 0.7, 8, H.darken(c[1], 0.15), 20);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(42);
// Sun
Draw.glow(ctx, w * 0.8, h * 0.15, h * 0.12, c[2]);
Draw.circle(ctx, w * 0.8, h * 0.15, h * 0.04, c[2]);
// Wildflowers across the meadow
for (var i = 0; i < 40; i++) {
var fx = rng() * w;
var fy = h * 0.62 + rng() * h * 0.25;
var fs = 3 + rng() * 5;
var sway = Math.sin(t * 1.5 + i * 0.7) * 2;
// Stem
ctx.strokeStyle = H.darken(c[1], 0.2);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(fx, fy); ctx.lineTo(fx + sway, fy - fs * 3); ctx.stroke();
// Petal
var flowerColor = rng() > 0.5 ? c[3] : c[2];
Draw.circle(ctx, fx + sway, fy - fs * 3, fs, flowerColor);
Draw.circle(ctx, fx + sway, fy - fs * 3, fs * 0.4, '#FFFFFF');
}
// Butterflies
for (var b = 0; b < 5; b++) {
var bx = (rng() * w + t * 20 * (b + 1)) % (w + 40) - 20;
var by = h * 0.3 + rng() * h * 0.3 + Math.sin(t * 2 + b) * 15;
var wingFlap = Math.abs(Math.sin(t * 6 + b * 2)) * 6;
ctx.fillStyle = H.rgba(c[3], 0.7);
ctx.beginPath(); ctx.ellipse(bx - wingFlap, by, wingFlap, 3, -0.3, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(bx + wingFlap, by, wingFlap, 3, 0.3, 0, Math.PI * 2); ctx.fill();
}
},
particles: { type: 'leaves', count: 10, color: '#FFB7C5' }
},
autumnLeaves: {
meta: { id: 'seasonal_autumnLeaves', mood: 'warm', colors: ['#2C1810', '#CC5500', '#DD8833', '#FFD700'], tags: ['autumn', 'fall', 'leaves', 'warm'], description: 'A warm autumn landscape with falling leaves and golden light', compatibleMusicMoods: ['mellow', 'peaceful', 'nostalgic'], recommendedFor: ['puzzle', 'story', 'rpg'] },
colors: ['#2C1810', '#CC5500', '#DD8833', '#FFD700'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#D4956B', '#E8A050', '#CC7733'], 90);
Draw.hills(ctx, w, h, h * 0.55, 5, '#8B6914', 15);
Draw.hills(ctx, w, h, h * 0.65, 7, '#6B4E0A', 25);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(77);
// Autumn trees
for (var i = 0; i < 12; i++) {
var tx = rng() * w;
var ty = h * 0.5 + rng() * h * 0.2;
var th = 40 + rng() * 50;
var tw = 4 + rng() * 4;
ctx.fillStyle = c[0];
ctx.fillRect(tx - tw / 2, ty - th * 0.4, tw, th * 0.5);
var leafCol = [c[1], c[2], c[3]][Math.floor(rng() * 3)];
var canopyR = th * 0.35 + Math.sin(t + i) * 2;
Draw.circle(ctx, tx, ty - th * 0.55, canopyR, leafCol);
Draw.circle(ctx, tx - canopyR * 0.5, ty - th * 0.45, canopyR * 0.7, H.darken(leafCol, 0.15));
Draw.circle(ctx, tx + canopyR * 0.4, ty - th * 0.5, canopyR * 0.6, H.lighten(leafCol, 0.1));
}
// Pumpkins on the ground
for (var p = 0; p < 6; p++) {
var px = rng() * w;
var py = h * 0.72 + rng() * h * 0.15;
var ps = 5 + rng() * 7;
Draw.circle(ctx, px, py, ps, '#E86800');
Draw.circle(ctx, px, py, ps * 0.9, '#FF7700');
ctx.fillStyle = '#336600';
ctx.fillRect(px - 1, py - ps - 3, 2, 4);
}
},
particles: { type: 'leaves', count: 30, color: '#DD6622' }
},
winterWonderland: {
meta: { id: 'seasonal_winterWonderland', mood: 'calm', colors: ['#1A2744', '#4A7CB5', '#C8E0F4', '#FFFFFF'], tags: ['winter', 'snow', 'cold', 'peaceful'], description: 'A serene winter landscape with snow-covered hills and frosty air', compatibleMusicMoods: ['peaceful', 'calm', 'ambient'], recommendedFor: ['puzzle', 'story', 'creative'] },
colors: ['#1A2744', '#4A7CB5', '#C8E0F4', '#FFFFFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
Draw.hills(ctx, w, h, h * 0.6, 5, c[2], 30);
Draw.hills(ctx, w, h, h * 0.7, 7, c[3], 40);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(55);
// Snow-covered evergreen trees
for (var i = 0; i < 15; i++) {
var tx = rng() * w;
var ty = h * 0.55 + rng() * h * 0.25;
var th = 25 + rng() * 45;
// Trunk
ctx.fillStyle = '#4A3728';
ctx.fillRect(tx - 2, ty - 5, 4, 10);
// Tree layers
for (var layer = 0; layer < 3; layer++) {
var lw = th * 0.4 * (1 - layer * 0.25);
var ly = ty - 5 - layer * th * 0.25;
ctx.fillStyle = '#1B4332';
ctx.beginPath(); ctx.moveTo(tx - lw, ly); ctx.lineTo(tx, ly - th * 0.3); ctx.lineTo(tx + lw, ly); ctx.closePath(); ctx.fill();
// Snow caps
ctx.fillStyle = H.rgba(c[3], 0.8);
ctx.beginPath(); ctx.moveTo(tx - lw * 0.7, ly - th * 0.08); ctx.lineTo(tx, ly - th * 0.3); ctx.lineTo(tx + lw * 0.7, ly - th * 0.08); ctx.closePath(); ctx.fill();
}
}
// Distant moon glow
var moonY = h * 0.12 + Math.sin(t * 0.1) * 3;
Draw.glow(ctx, w * 0.2, moonY, h * 0.08, c[3]);
Draw.circle(ctx, w * 0.2, moonY, h * 0.03, c[3]);
},
particles: { type: 'snow', count: 80, color: '#FFFFFF' }
},
summerBeach: {
meta: { id: 'seasonal_summerBeach', mood: 'energetic', colors: ['#0077BE', '#00BFFF', '#F4D03F', '#FF6347'], tags: ['summer', 'beach', 'ocean', 'sunny'], description: 'A vibrant summer beach scene with rolling waves and warm sand', compatibleMusicMoods: ['happy', 'upbeat', 'energetic'], recommendedFor: ['action', 'sports', 'racing'] },
colors: ['#0077BE', '#00BFFF', '#F4D03F', '#FF6347'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Sand
Draw.rect(ctx, 0, h * 0.65, w, h * 0.35, c[2]);
// Shore wave line
ctx.strokeStyle = H.rgba('#FFFFFF', 0.4);
ctx.lineWidth = 2;
ctx.beginPath();
for (var x = 0; x < w; x += 3) {
var wy = h * 0.65 + Math.sin(x * 0.015 + t * 2) * 5 + Math.sin(x * 0.03 + t * 3) * 2;
x === 0 ? ctx.moveTo(x, wy) : ctx.lineTo(x, wy);
}
ctx.stroke();
Draw.water(ctx, w, h, h * 0.45, H.rgba(c[0], 0.4), 4, 50);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(88);
// Sun
Draw.glow(ctx, w * 0.75, h * 0.1, h * 0.15, '#FFD700');
Draw.circle(ctx, w * 0.75, h * 0.1, h * 0.05, '#FFE44D');
// Palm trees
for (var i = 0; i < 4; i++) {
var px = w * 0.1 + rng() * w * 0.35;
var py = h * 0.65;
var ph = 60 + rng() * 40;
var lean = (rng() - 0.5) * 0.3;
// Curved trunk
ctx.strokeStyle = '#8B6914';
ctx.lineWidth = 6;
ctx.beginPath(); ctx.moveTo(px, py);
ctx.quadraticCurveTo(px + lean * ph, py - ph * 0.5, px + lean * ph * 1.5, py - ph);
ctx.stroke();
// Fronds
var topX = px + lean * ph * 1.5, topY = py - ph;
for (var f = 0; f < 6; f++) {
var angle = (f / 6) * Math.PI * 2 + Math.sin(t * 0.8 + f) * 0.1;
var fLen = 25 + rng() * 20;
ctx.strokeStyle = '#228B22';
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(topX, topY);
ctx.quadraticCurveTo(topX + Math.cos(angle) * fLen * 0.6, topY + Math.sin(angle) * fLen * 0.3, topX + Math.cos(angle) * fLen, topY + Math.abs(Math.sin(angle)) * fLen * 0.5 + 8);
ctx.stroke();
}
}
// Beach umbrella
var ux = w * 0.6, uy = h * 0.58;
ctx.fillStyle = '#8B6914';
ctx.fillRect(ux - 1.5, uy, 3, h * 0.12);
ctx.fillStyle = c[3];
ctx.beginPath(); ctx.arc(ux, uy, 25, Math.PI, 0); ctx.fill();
ctx.fillStyle = '#FFFFFF';
ctx.beginPath(); ctx.arc(ux, uy, 25, Math.PI, Math.PI + Math.PI / 4); ctx.lineTo(ux, uy); ctx.fill();
ctx.beginPath(); ctx.arc(ux, uy, 25, Math.PI + Math.PI / 2, Math.PI + Math.PI * 3 / 4); ctx.lineTo(ux, uy); ctx.fill();
},
particles: { type: 'sparkles', count: 20, color: '#FFE44D' }
},
harvest: {
meta: { id: 'seasonal_harvest', mood: 'warm', colors: ['#5C3317', '#D4A017', '#8B4513', '#FFD700'], tags: ['harvest', 'farm', 'autumn', 'golden'], description: 'Golden harvest fields with wheat rows and a warm sunset', compatibleMusicMoods: ['mellow', 'peaceful', 'warm'], recommendedFor: ['puzzle', 'story', 'creative'] },
colors: ['#5C3317', '#D4A017', '#8B4513', '#FFD700'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#E8913A', '#F4C54D', c[1]], 90);
// Field rows
Draw.rect(ctx, 0, h * 0.55, w, h * 0.45, '#B8860B');
for (var r = 0; r < 8; r++) {
var ry = h * 0.58 + r * h * 0.05;
ctx.fillStyle = H.rgba(H.darken(c[1], 0.1 + r * 0.03), 0.6);
ctx.fillRect(0, ry, w, 2);
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(99);
// Wheat stalks
for (var i = 0; i < 60; i++) {
var wx = rng() * w;
var wy = h * 0.56 + rng() * h * 0.35;
var wh = 15 + rng() * 25;
var sway = Math.sin(t * 1.2 + i * 0.5) * 3;
ctx.strokeStyle = H.rgba(c[1], 0.7);
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(wx, wy); ctx.lineTo(wx + sway, wy - wh); ctx.stroke();
// Wheat head
ctx.fillStyle = c[3];
ctx.beginPath(); ctx.ellipse(wx + sway, wy - wh - 3, 2, 5, 0, 0, Math.PI * 2); ctx.fill();
}
// Sunset glow
Draw.glow(ctx, w * 0.5, h * 0.12, h * 0.18, '#FF8C00');
Draw.circle(ctx, w * 0.5, h * 0.12, h * 0.06, '#FFD700');
// Barn silhouette
var bx = w * 0.78, by = h * 0.48, bw = 50, bh = 35;
ctx.fillStyle = c[2];
ctx.fillRect(bx, by, bw, bh);
ctx.beginPath(); ctx.moveTo(bx - 5, by); ctx.lineTo(bx + bw / 2, by - 25); ctx.lineTo(bx + bw + 5, by); ctx.closePath();
ctx.fillStyle = H.darken(c[2], 0.2);
ctx.fill();
// Silo
ctx.fillStyle = c[2];
ctx.fillRect(bx + bw + 8, by - 10, 12, bh + 10);
ctx.beginPath(); ctx.arc(bx + bw + 14, by - 10, 6, 0, Math.PI * 2); ctx.fill();
},
particles: { type: 'dust', count: 20, color: '#DDCC88' }
},
blossom: {
meta: { id: 'seasonal_blossom', mood: 'peaceful', colors: ['#FFB7C5', '#FF69B4', '#FFF0F5', '#98FB98'], tags: ['spring', 'cherry', 'blossom', 'pink'], description: 'Gentle cherry blossom petals drifting under pink-tinted skies', compatibleMusicMoods: ['peaceful', 'calm', 'romantic'], recommendedFor: ['puzzle', 'story', 'creative', 'music'] },
colors: ['#FFB7C5', '#FF69B4', '#FFF0F5', '#98FB98'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[2], c[0], H.lighten(c[3], 0.3)], 90);
Draw.hills(ctx, w, h, h * 0.7, 6, c[3], 12);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(33);
// Cherry blossom trees
for (var i = 0; i < 5; i++) {
var tx = w * 0.1 + rng() * w * 0.8;
var ty = h * 0.65 + rng() * h * 0.1;
var th = 50 + rng() * 40;
// Trunk and branches
ctx.strokeStyle = '#5C3A1E';
ctx.lineWidth = 4 + rng() * 3;
ctx.beginPath(); ctx.moveTo(tx, ty); ctx.quadraticCurveTo(tx + rng() * 10 - 5, ty - th * 0.5, tx, ty - th); ctx.stroke();
// Branch clusters
for (var b = 0; b < 4; b++) {
var bAngle = (rng() - 0.5) * Math.PI * 0.8;
var bLen = 15 + rng() * 25;
var bStartY = ty - th * (0.5 + rng() * 0.4);
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(tx, bStartY);
ctx.lineTo(tx + Math.cos(bAngle) * bLen, bStartY + Math.sin(bAngle) * bLen - 10);
ctx.stroke();
}
// Blossom clouds
for (var bc = 0; bc < 6; bc++) {
var bx = tx + (rng() - 0.5) * th * 0.8;
var by = ty - th * (0.4 + rng() * 0.5);
var br = 8 + rng() * 15;
var pulseR = br + Math.sin(t * 0.8 + i + bc) * 1.5;
Draw.circle(ctx, bx, by, pulseR, H.rgba(c[0], 0.5 + rng() * 0.3));
Draw.circle(ctx, bx + br * 0.3, by - br * 0.2, pulseR * 0.7, H.rgba(c[1], 0.3));
}
}
// Soft light
Draw.glow(ctx, w * 0.5, h * 0.08, h * 0.12, '#FFF5EE');
},
particles: { type: 'leaves', count: 25, color: '#FFB7C5' }
},
snowfall: {
meta: { id: 'seasonal_snowfall', mood: 'calm', colors: ['#0D1B2A', '#1B2838', '#415A77', '#E0E1DD'], tags: ['winter', 'night', 'snow', 'dark'], description: 'Heavy snowfall over a dark winter night with distant lights', compatibleMusicMoods: ['calm', 'ambient', 'mysterious'], recommendedFor: ['puzzle', 'story', 'rpg'] },
colors: ['#0D1B2A', '#1B2838', '#415A77', '#E0E1DD'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.mountains(ctx, w, h, h * 0.5, 6, c[2], 60);
Draw.hills(ctx, w, h, h * 0.7, 5, H.lighten(c[2], 0.2), 70);
// Snow ground
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, H.rgba(c[3], 0.3));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(44);
// Distant village lights
for (var i = 0; i < 8; i++) {
var lx = rng() * w;
var ly = h * 0.55 + rng() * h * 0.15;
var twinkle = 0.3 + Math.sin(t * 2 + i * 1.5) * 0.2;
Draw.glow(ctx, lx, ly, 8, H.rgba('#FFD700', twinkle));
Draw.circle(ctx, lx, ly, 1.5, H.rgba('#FFE088', twinkle + 0.3));
}
// Bare tree silhouettes
for (var tr = 0; tr < 6; tr++) {
var tx = rng() * w;
var ty = h * 0.75 + rng() * h * 0.05;
var th = 30 + rng() * 40;
ctx.strokeStyle = H.rgba(c[0], 0.6);
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(tx, ty - th); ctx.stroke();
// Branches
for (var br = 0; br < 4; br++) {
var brY = ty - th * (0.3 + br * 0.18);
var brDir = br % 2 === 0 ? 1 : -1;
var brLen = 8 + rng() * 15;
ctx.beginPath(); ctx.moveTo(tx, brY);
ctx.lineTo(tx + brDir * brLen, brY - brLen * 0.6);
ctx.stroke();
}
}
// Snow on ground — gentle undulation
ctx.fillStyle = H.rgba(c[3], 0.15);
ctx.beginPath(); ctx.moveTo(0, h);
for (var x = 0; x <= w; x += 20) {
ctx.lineTo(x, h * 0.78 + Math.sin(x * 0.03 + t * 0.3) * 4);
}
ctx.lineTo(w, h); ctx.closePath(); ctx.fill();
},
particles: { type: 'snow', count: 120, color: '#E0E1DD' }
},
tropical: {
meta: { id: 'seasonal_tropical', mood: 'energetic', colors: ['#00CED1', '#FF6347', '#228B22', '#FFD700'], tags: ['summer', 'tropical', 'jungle', 'vibrant'], description: 'A lush tropical scene with dense foliage and vivid colors', compatibleMusicMoods: ['energetic', 'upbeat', 'happy'], recommendedFor: ['action', 'racing', 'sports', 'pet'] },
colors: ['#00CED1', '#FF6347', '#228B22', '#FFD700'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [H.lighten(c[0], 0.2), c[0]], 90);
Draw.hills(ctx, w, h, h * 0.5, 4, c[2], 35);
Draw.hills(ctx, w, h, h * 0.6, 6, H.darken(c[2], 0.2), 45);
// Jungle floor
Draw.rect(ctx, 0, h * 0.75, w, h * 0.25, H.darken(c[2], 0.35));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(66);
// Tropical flowers
for (var i = 0; i < 20; i++) {
var fx = rng() * w;
var fy = h * 0.55 + rng() * h * 0.3;
var fs = 4 + rng() * 6;
var bloom = fs + Math.sin(t + i * 0.8) * 1;
var flColor = rng() > 0.5 ? c[1] : c[3];
// Petals
ctx.save(); ctx.translate(fx, fy);
for (var p = 0; p < 5; p++) {
ctx.rotate(Math.PI * 2 / 5);
ctx.fillStyle = H.rgba(flColor, 0.7);
ctx.beginPath(); ctx.ellipse(0, -bloom, bloom * 0.4, bloom, 0, 0, Math.PI * 2); ctx.fill();
}
ctx.restore();
Draw.circle(ctx, fx, fy, bloom * 0.3, '#FFFFFF');
}
// Large palm fronds at edges
for (var f = 0; f < 3; f++) {
var side = f % 2 === 0 ? 0 : w;
var dir = f % 2 === 0 ? 1 : -1;
var fy2 = h * 0.05 + f * h * 0.15;
var sway = Math.sin(t * 0.5 + f) * 8;
ctx.strokeStyle = c[2];
ctx.lineWidth = 4;
for (var fr = 0; fr < 4; fr++) {
var angle = (0.2 + fr * 0.3) * dir;
ctx.beginPath(); ctx.moveTo(side, fy2 + fr * 20);
ctx.quadraticCurveTo(side + dir * (60 + sway), fy2 + fr * 20 + 30, side + dir * (100 + sway), fy2 + fr * 20 + 60);
ctx.stroke();
}
}
// Parrots
for (var b = 0; b < 3; b++) {
var bx = (rng() * w + t * 15 * (b + 1)) % (w + 30) - 15;
var by = h * 0.15 + rng() * h * 0.2 + Math.sin(t * 1.5 + b * 2) * 10;
var wingF = Math.abs(Math.sin(t * 5 + b * 3)) * 5;
ctx.fillStyle = [c[1], c[3], '#00AA44'][b % 3];
ctx.beginPath(); ctx.ellipse(bx, by, 4, 2.5, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = H.rgba('#000000', 0.4);
ctx.beginPath(); ctx.ellipse(bx - wingF * 0.8, by - 1, wingF, 2, -0.4, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(bx + wingF * 0.8, by - 1, wingF, 2, 0.4, 0, Math.PI * 2); ctx.fill();
}
},
particles: { type: 'fireflies', count: 15, color: '#CCFF44' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,232 @@
// sky.js — Sky background theme (8 variants)
// Depends on backgroundEngine.js being loaded first
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
function drawCloud(ctx, x, y, s, col, a) {
ctx.fillStyle = H.rgba(col, a);
ctx.beginPath(); ctx.ellipse(x, y, s, s * 0.45, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(x - s * 0.45, y + s * 0.15, s * 0.55, s * 0.38, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(x + s * 0.4, y + s * 0.12, s * 0.5, s * 0.35, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(x + s * 0.15, y - s * 0.2, s * 0.4, s * 0.3, 0, 0, Math.PI * 2); ctx.fill();
}
function drawBird(ctx, x, y, s, col) {
ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.beginPath();
ctx.moveTo(x - s, y + s * 0.3); ctx.quadraticCurveTo(x - s * 0.3, y - s * 0.5, x, y);
ctx.quadraticCurveTo(x + s * 0.3, y - s * 0.5, x + s, y + s * 0.3); ctx.stroke();
}
engine.registerTheme('sky', {
clouds: {
meta: { id: 'sky_clouds', mood: 'peaceful', colors: ['#4A90D9', '#87CEEB', '#FFFFFF', '#F0F8FF'], tags: ['clouds', 'blue', 'daytime', 'fluffy'], description: 'A bright blue sky filled with drifting puffy white clouds', compatibleMusicMoods: ['peaceful', 'happy', 'calm'], recommendedFor: ['puzzle', 'creative', 'pet'] },
colors: ['#4A90D9', '#87CEEB', '#FFFFFF', '#F0F8FF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.lighten(c[1], 0.15)], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(20);
for (var i = 0; i < 8; i++) drawCloud(ctx, (rng() * w * 1.4 - w * 0.2 + t * 3) % (w + 120) - 60, h * 0.1 + rng() * h * 0.25, 25 + rng() * 35, c[3], 0.25 + rng() * 0.15);
for (var j = 0; j < 6; j++) drawCloud(ctx, (rng() * w * 1.4 - w * 0.2 + t * 7) % (w + 200) - 100, h * 0.2 + rng() * h * 0.3, 40 + rng() * 55, c[2], 0.4 + rng() * 0.25);
for (var k = 0; k < 4; k++) {
var nx = (rng() * w * 1.6 - w * 0.3 + t * 12) % (w + 300) - 150, ny = h * 0.35 + rng() * h * 0.35, ns = 60 + rng() * 80;
drawCloud(ctx, nx, ny, ns, c[2], 0.6 + rng() * 0.3);
drawCloud(ctx, nx + 5, ny + ns * 0.5, ns * 0.8, c[0], 0.05);
}
for (var b = 0; b < 4; b++) drawBird(ctx, (rng() * w + t * 18 * (b + 1)) % (w + 40) - 20, h * 0.08 + rng() * h * 0.2 + Math.sin(t * 1.5 + b * 2) * 5, 4 + rng() * 4, H.rgba(c[0], 0.3));
},
particles: { type: 'dust', count: 10, color: '#FFFFFF' }
},
flyingHigh: {
meta: { id: 'sky_flyingHigh', mood: 'energetic', colors: ['#1E3A6E', '#4682B4', '#FFD700', '#FFFFFF'], tags: ['flying', 'altitude', 'wind', 'bright'], description: 'High altitude perspective with streaking wind lines and brilliant sun', compatibleMusicMoods: ['energetic', 'upbeat', 'adventurous'], recommendedFor: ['action', 'racing', 'sports'] },
colors: ['#1E3A6E', '#4682B4', '#FFD700', '#FFFFFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.glow(ctx, w * 0.7, h * 0.2, h * 0.2, c[2]); Draw.circle(ctx, w * 0.7, h * 0.2, h * 0.06, c[2]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(40);
ctx.lineWidth = 1;
for (var i = 0; i < 20; i++) {
var sy = rng() * h, sx = (rng() * w * 2 - w * 0.5 + t * 80 * (0.5 + rng())) % (w + 100) - 50;
ctx.strokeStyle = H.rgba(c[3], 0.08 + rng() * 0.12);
ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx + 30 + rng() * 80, sy - 1); ctx.stroke();
}
for (var j = 0; j < 6; j++) {
ctx.fillStyle = H.rgba(c[3], 0.08 + rng() * 0.08);
ctx.beginPath(); ctx.ellipse((rng() * w * 1.5 + t * 15) % (w + 200) - 100, h * 0.1 + rng() * h * 0.4, 80 + rng() * 60, 5 + rng() * 8, 0, 0, Math.PI * 2); ctx.fill();
}
ctx.save(); ctx.globalCompositeOperation = 'lighter';
for (var r = 0; r < 8; r++) {
var ang = (r / 8) * Math.PI * 2 + t * 0.05, rL = h * 0.15 + Math.sin(t * 0.5 + r) * h * 0.03;
ctx.strokeStyle = H.rgba(c[2], 0.06); ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(w * 0.7 + Math.cos(ang) * h * 0.07, h * 0.2 + Math.sin(ang) * h * 0.07); ctx.lineTo(w * 0.7 + Math.cos(ang) * rL, h * 0.2 + Math.sin(ang) * rL); ctx.stroke();
}
ctx.restore();
},
particles: { type: 'sparkles', count: 15, color: '#FFFFFF' }
},
hotAirBalloon: {
meta: { id: 'sky_hotAirBalloon', mood: 'cheerful', colors: ['#87CEEB', '#FF6347', '#FFD700', '#FFFFFF'], tags: ['balloon', 'colorful', 'gentle', 'whimsical'], description: 'Colorful hot air balloons floating gently across a warm sky', compatibleMusicMoods: ['happy', 'peaceful', 'cheerful'], recommendedFor: ['puzzle', 'creative', 'pet', 'music'] },
colors: ['#87CEEB', '#FF6347', '#FFD700', '#FFFFFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], H.lighten(c[0], 0.25), '#FFEEDD'], 90);
Draw.clouds(ctx, w, h, h * 0.7, 6, c[3], 30);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(52);
var bals = [{ c1: c[1], c2: c[2] }, { c1: '#8844CC', c2: '#44BBEE' }, { c1: '#44AA44', c2: '#FFAA33' }, { c1: '#EE5588', c2: '#FFDD44' }];
for (var i = 0; i < 4; i++) {
var bx = w * 0.15 + rng() * w * 0.7 + Math.sin(t * 0.3 + i * 2) * 15;
var by = h * 0.15 + rng() * h * 0.4 + Math.sin(t * 0.5 + i * 1.5) * 10, bs = 18 + rng() * 22;
ctx.fillStyle = bals[i].c1; ctx.beginPath(); ctx.ellipse(bx, by, bs, bs * 1.2, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = bals[i].c2; ctx.beginPath(); ctx.ellipse(bx, by, bs * 0.35, bs * 1.18, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = H.rgba('#FFF', 0.2); ctx.beginPath(); ctx.ellipse(bx - bs * 0.25, by - bs * 0.3, bs * 0.25, bs * 0.4, -0.3, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = H.rgba('#444', 0.4); ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(bx - bs * 0.3, by + bs * 1.1); ctx.lineTo(bx - 4, by + bs * 1.6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(bx + bs * 0.3, by + bs * 1.1); ctx.lineTo(bx + 4, by + bs * 1.6); ctx.stroke();
ctx.fillStyle = '#8B6914'; ctx.fillRect(bx - 5, by + bs * 1.55, 10, 7);
}
for (var k = 0; k < 5; k++) drawCloud(ctx, (rng() * w * 1.3 + t * 5) % (w + 150) - 75, h * 0.3 + rng() * h * 0.35, 30 + rng() * 40, c[3], 0.2 + rng() * 0.15);
for (var b = 0; b < 3; b++) drawBird(ctx, (rng() * w + t * 22) % (w + 30) - 15, h * 0.1 + rng() * h * 0.15, 3 + rng() * 3, H.rgba('#555', 0.25));
},
particles: { type: 'dust', count: 8, color: '#FFFFFF' }
},
airplaneView: {
meta: { id: 'sky_airplaneView', mood: 'calm', colors: ['#1A3366', '#336699', '#FFFFFF', '#AACCEE'], tags: ['airplane', 'altitude', 'above-clouds', 'travel'], description: 'View from an airplane window looking down at a sea of clouds', compatibleMusicMoods: ['calm', 'peaceful', 'ambient'], recommendedFor: ['puzzle', 'story', 'creative'] },
colors: ['#1A3366', '#336699', '#FFFFFF', '#AACCEE'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.hills(ctx, w, h, h * 0.55, 12, H.rgba(c[3], 0.6), 33);
Draw.hills(ctx, w, h, h * 0.57, 10, H.rgba(c[2], 0.5), 44);
Draw.rect(ctx, 0, h * 0.65, w, h * 0.35, H.rgba(c[2], 0.7));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(64);
for (var i = 0; i < 12; i++) drawCloud(ctx, (rng() * w * 1.4 + t * 4) % (w + 100) - 50, h * 0.58 + rng() * h * 0.25, 30 + rng() * 60, c[2], 0.3 + rng() * 0.3);
// Wing
ctx.fillStyle = '#C0C0C0'; ctx.beginPath(); ctx.moveTo(-10, h * 0.5); ctx.lineTo(w * 0.25, h * 0.48); ctx.lineTo(w * 0.28, h * 0.5); ctx.lineTo(w * 0.12, h * 0.52); ctx.lineTo(-10, h * 0.52); ctx.closePath(); ctx.fill();
ctx.fillStyle = H.rgba('#FFF', 0.3); ctx.beginPath(); ctx.moveTo(-10, h * 0.5); ctx.lineTo(w * 0.25, h * 0.48); ctx.lineTo(w * 0.22, h * 0.49); ctx.lineTo(-10, h * 0.505); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#D0D0D0'; ctx.beginPath(); ctx.moveTo(w * 0.25, h * 0.48); ctx.lineTo(w * 0.27, h * 0.44); ctx.lineTo(w * 0.28, h * 0.48); ctx.closePath(); ctx.fill();
var blink = Math.sin(t * 3) > 0.7 ? 0.9 : 0.1;
Draw.glow(ctx, w * 0.26, h * 0.485, 5, H.rgba('#FF0000', blink));
Draw.circle(ctx, w * 0.26, h * 0.485, 1.5, H.rgba('#FF3333', blink + 0.2));
Draw.glow(ctx, w * 0.8, h * 0.08, h * 0.12, '#FFE88A'); Draw.circle(ctx, w * 0.8, h * 0.08, h * 0.035, '#FFE44D');
},
particles: { type: 'dust', count: 5, color: '#FFFFFF' }
},
stratosphere: {
meta: { id: 'sky_stratosphere', mood: 'mysterious', colors: ['#0A0A2E', '#1A1A5E', '#4444AA', '#8888DD'], tags: ['stratosphere', 'space', 'high-altitude', 'curvature'], description: 'The edge of space where the atmosphere fades and Earth curves below', compatibleMusicMoods: ['mysterious', 'ambient', 'calm'], recommendedFor: ['story', 'rpg', 'puzzle'] },
colors: ['#0A0A2E', '#1A1A5E', '#4444AA', '#8888DD'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#000010', c[0], c[1]], 90);
var gr = ctx.createLinearGradient(0, h * 0.75, 0, h);
gr.addColorStop(0, 'transparent'); gr.addColorStop(0.3, H.rgba(c[2], 0.15)); gr.addColorStop(0.7, H.rgba(c[3], 0.25)); gr.addColorStop(1, H.rgba('#AABBEE', 0.35));
ctx.fillStyle = gr; ctx.fillRect(0, h * 0.75, w, h * 0.25);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(85);
Draw.stars(ctx, w, h * 0.5, 80, '#FFFFFF', 85, [0.3, 1.8]);
ctx.strokeStyle = H.rgba(c[3], 0.3); ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-20, h * 0.88); ctx.quadraticCurveTo(w / 2, h * 0.82, w + 20, h * 0.88); ctx.stroke();
for (var i = 0; i < 3; i++) { ctx.fillStyle = H.rgba(c[2 + (i > 1 ? 0 : 1)], 0.06 - i * 0.015 + Math.sin(t * 0.3 + i) * 0.01); ctx.fillRect(0, h * 0.78 + i * h * 0.04, w, h * 0.03); }
var satX = w * 0.35 + Math.sin(t * 0.2) * 30, satY = h * 0.3 + Math.cos(t * 0.15) * 10;
ctx.fillStyle = '#AAA'; ctx.fillRect(satX - 2, satY - 1, 4, 2);
ctx.fillStyle = '#4466AA'; ctx.fillRect(satX - 10, satY - 0.5, 7, 1); ctx.fillRect(satX + 3, satY - 0.5, 7, 1);
var gl = Math.max(0, Math.sin(t * 2));
if (gl > 0.9) Draw.glow(ctx, satX, satY, 8, H.rgba('#FFF', (gl - 0.9) * 10));
var sp = (t * 0.3) % 6;
if (sp < 0.5) { var p = sp / 0.5; ctx.strokeStyle = H.rgba('#FFF', 0.6 * (1 - p)); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(w * 0.8 - p * w * 0.3, h * 0.1 + p * h * 0.2); ctx.lineTo(w * 0.8 - p * w * 0.3 + 20, h * 0.1 + p * h * 0.2 - 8); ctx.stroke(); }
},
particles: { type: 'stars', count: 40, color: '#FFFFFF' }
},
birdsEye: {
meta: { id: 'sky_birdsEye', mood: 'peaceful', colors: ['#5588BB', '#88BBDD', '#228B22', '#DEB887'], tags: ['aerial', 'landscape', 'birds-eye', 'fields'], description: 'A bird\'s eye view looking down at patchwork fields and winding rivers', compatibleMusicMoods: ['peaceful', 'calm', 'happy'], recommendedFor: ['puzzle', 'creative', 'story'] },
colors: ['#5588BB', '#88BBDD', '#228B22', '#DEB887'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.rect(ctx, 0, h * 0.4, w, h * 0.6, c[2]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(73), fC = [c[2], H.darken(c[2], 0.15), H.lighten(c[2], 0.1), c[3], '#AAB86B', '#8CAA58'];
var gW = 6, gH = 5;
for (var gy = 0; gy < gH; gy++) for (var gx = 0; gx < gW; gx++) { ctx.fillStyle = fC[Math.floor(rng() * fC.length)]; ctx.fillRect(gx * (w / gW) + 1, h * 0.4 + gy * (h * 0.6 / gH) + 1, w / gW - 2, h * 0.6 / gH - 2); }
ctx.strokeStyle = H.rgba('#556633', 0.3); ctx.lineWidth = 1.5;
for (var lx = 0; lx <= gW; lx++) { ctx.beginPath(); ctx.moveTo(lx * (w / gW), h * 0.4); ctx.lineTo(lx * (w / gW), h); ctx.stroke(); }
for (var ly = 0; ly <= gH; ly++) { var yy = h * 0.4 + ly * (h * 0.6 / gH); ctx.beginPath(); ctx.moveTo(0, yy); ctx.lineTo(w, yy); ctx.stroke(); }
ctx.strokeStyle = '#4488BB'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(w * 0.3, h * 0.4);
for (var s = 1; s <= 8; s++) ctx.lineTo(w * 0.3 + Math.sin(s * 0.9 + 0.5) * w * 0.12, h * 0.4 + (h * 0.6 / 8) * s);
ctx.stroke();
for (var i = 0; i < 5; i++) drawCloud(ctx, (rng() * w * 1.3 + t * 6) % (w + 150) - 75, h * 0.05 + rng() * h * 0.25, 50 + rng() * 70, '#FFF', 0.35 + rng() * 0.25);
for (var cs = 0; cs < 4; cs++) { ctx.fillStyle = H.rgba('#000', 0.06); ctx.beginPath(); ctx.ellipse((rng() * w * 1.3 + t * 6) % (w + 100) - 50, h * 0.5 + rng() * h * 0.35, 50 + rng() * 40, 25 + rng() * 15, 0, 0, Math.PI * 2); ctx.fill(); }
},
particles: { type: 'dust', count: 6, color: '#FFFFFF' }
},
horizon: {
meta: { id: 'sky_horizon', mood: 'warm', colors: ['#1A0533', '#CC4488', '#FF8844', '#FFD700'], tags: ['sunset', 'horizon', 'golden', 'dramatic'], description: 'A dramatic sunset horizon with layers of warm orange, pink, and gold', compatibleMusicMoods: ['mellow', 'warm', 'nostalgic', 'peaceful'], recommendedFor: ['story', 'music', 'creative', 'rpg'] },
colors: ['#1A0533', '#CC4488', '#FF8844', '#FFD700'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2], c[3], H.lighten(c[3], 0.3)], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(48), sunY = h * 0.7 + Math.sin(t * 0.1) * 3;
Draw.glow(ctx, w * 0.5, sunY, h * 0.25, H.rgba(c[3], 0.3));
Draw.glow(ctx, w * 0.5, sunY, h * 0.12, H.rgba(c[2], 0.4));
Draw.circle(ctx, w * 0.5, sunY, h * 0.06, c[3]);
for (var i = 0; i < 8; i++) drawCloud(ctx, (rng() * w * 1.2 + t * 2) % (w + 80) - 40, h * 0.62 + rng() * h * 0.08, 30 + rng() * 50, H.blendColor(c[1], c[0], rng() * 0.5), 0.5 + rng() * 0.3);
for (var j = 0; j < 5; j++) { ctx.fillStyle = H.rgba(c[1], 0.08 + rng() * 0.06); ctx.beginPath(); ctx.ellipse(rng() * w, h * 0.1 + rng() * h * 0.25, 60 + rng() * 80, 4 + rng() * 6, 0, 0, Math.PI * 2); ctx.fill(); }
// Silhouette landscape
var lr = H.seededRandom(100); ctx.fillStyle = H.rgba('#000', 0.6); ctx.beginPath(); ctx.moveTo(0, h);
for (var x = 0; x <= w; x += 8) ctx.lineTo(x, h * 0.82 + lr() * h * 0.04 - Math.sin(x * 0.008) * h * 0.03);
ctx.lineTo(w, h); ctx.closePath(); ctx.fill();
for (var b = 0; b < 6; b++) drawBird(ctx, w * 0.3 + rng() * w * 0.4 + Math.sin(t * 0.5 + b) * 8, h * 0.5 + rng() * h * 0.1 + Math.sin(t * 1.2 + b * 1.5) * 4, 3 + rng() * 3, H.rgba('#000', 0.4));
},
particles: { type: 'dust', count: 8, color: '#FFD700' }
},
starfield: {
meta: { id: 'sky_starfield', mood: 'calm', colors: ['#000011', '#0A0A2A', '#1A1A44', '#FFFFFF'], tags: ['night', 'stars', 'galaxy', 'space'], description: 'A clear night sky bursting with stars and a faint galactic band', compatibleMusicMoods: ['calm', 'ambient', 'mysterious'], recommendedFor: ['puzzle', 'story', 'rpg', 'music'] },
colors: ['#000011', '#0A0A2A', '#1A1A44', '#FFFFFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
ctx.save(); ctx.globalCompositeOperation = 'lighter';
var rng = H.seededRandom(100);
for (var i = 0; i < 200; i++) {
var p = i / 200; ctx.fillStyle = H.rgba('#8888CC', 0.05 + rng() * 0.1);
ctx.beginPath(); ctx.arc(p * w * 1.3 - w * 0.15, h * 0.2 + p * h * 0.6 + (rng() - 0.5) * h * 0.12, 0.5 + rng() * 1.5, 0, Math.PI * 2); ctx.fill();
}
Draw.glow(ctx, w * 0.5, h * 0.5, h * 0.2, H.rgba('#6655AA', 0.04));
ctx.restore();
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(77);
Draw.stars(ctx, w, h, 150, '#FFFFFF', 77, [0.3, 2]);
Draw.stars(ctx, w, h, 50, '#AABBFF', 88, [0.8, 2.5]);
Draw.stars(ctx, w, h, 20, '#FFDDAA', 99, [1, 3]);
for (var i = 0; i < 5; i++) {
var sx = rng() * w, sy = rng() * h, tw = 0.4 + Math.sin(t * 2 + i * 2.5) * 0.3;
Draw.glow(ctx, sx, sy, 6 + tw * 4, H.rgba('#FFF', tw * 0.3));
Draw.circle(ctx, sx, sy, 1.5 + tw, H.rgba('#FFF', 0.6 + tw * 0.3));
}
var st = (t * 0.5) % 8;
if (st < 0.4) {
var sp = st / 0.4, sx2 = H.lerp(w * 0.7, w * 0.3, sp), sy2 = H.lerp(h * 0.1, h * 0.4, sp);
ctx.strokeStyle = H.rgba('#FFF', 0.7 * (1 - sp)); ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(sx2, sy2); ctx.lineTo(sx2 + 25, sy2 - 10); ctx.stroke();
Draw.glow(ctx, sx2, sy2, 4, H.rgba('#FFF', 0.5 * (1 - sp)));
}
},
particles: { type: 'stars', count: 100, color: '#FFFFFF' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,275 @@
// space.js — Space theme for procedural background system
// 8 variants: nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('space', {
nebula: {
meta: { id: 'space_nebula', mood: 'wonder', colors: ['#0B0B2B', '#1A0A3E', '#3D1A78', '#7B2FBE'], tags: ['cosmic', 'colorful', 'calm'], description: 'Swirling nebula clouds with twinkling stars', compatibleMusicMoods: ['ambient', 'dreamy', 'epic'], recommendedFor: ['puzzle', 'story', 'creative'] },
colors: ['#0B0B2B', '#1A0A3E', '#3D1A78', '#7B2FBE'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
var rng = H.seededRandom(101);
for (var i = 0; i < 5; i++) {
var cx = rng() * w, cy = rng() * h;
var r = 80 + rng() * 200;
var pulse = 0.9 + Math.sin(t * 0.3 + i) * 0.1;
Draw.glow(ctx, cx, cy, r * pulse, c[3]);
Draw.glow(ctx, cx + 40, cy - 30, r * 0.6 * pulse, c[2]);
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
Draw.stars(ctx, w, h, 180, '#FFFFFF', 10, [0.5, 2]);
Draw.stars(ctx, w, h, 40, c[3], 20, [1, 3]);
var rng = H.seededRandom(102);
for (var i = 0; i < 3; i++) {
var x = rng() * w, y = rng() * h * 0.8;
var flicker = 0.5 + Math.sin(t * 2 + i * 1.7) * 0.3;
Draw.glow(ctx, x, y, 4 + rng() * 4, H.rgba('#FFFFFF', flicker));
}
},
particles: { type: 'sparkles', count: 40, color: '#CCBBFF' }
},
asteroidField: {
meta: { id: 'space_asteroidField', mood: 'tense', colors: ['#0A0A14', '#1C1C2E', '#4A3A2A', '#8B7355'], tags: ['rocky', 'danger', 'dark'], description: 'Rocky asteroids drifting through dark space', compatibleMusicMoods: ['tense', 'action', 'ambient'], recommendedFor: ['action', 'racing', 'sports'] },
colors: ['#0A0A14', '#1C1C2E', '#4A3A2A', '#8B7355'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.stars(ctx, w, h, 100, '#FFFFFF', 30, [0.3, 1.5]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(200);
for (var i = 0; i < 18; i++) {
var ax = (rng() * w + t * (5 + rng() * 15)) % (w + 100) - 50;
var ay = rng() * h;
var ar = 8 + rng() * 35;
var sides = 5 + Math.floor(rng() * 4);
var rot = rng() * Math.PI * 2 + t * (rng() - 0.5) * 0.2;
var shade = H.blendColor(c[2], c[3], rng());
Draw.polygon(ctx, ax, ay, ar, sides, shade, rot);
Draw.polygon(ctx, ax - ar * 0.15, ay - ar * 0.15, ar * 0.85, sides, H.lighten(shade, 0.15), rot);
}
},
particles: { type: 'dust', count: 25, color: '#887766' }
},
deepSpace: {
meta: { id: 'space_deepSpace', mood: 'serene', colors: ['#000005', '#050510', '#0A0A20', '#111133'], tags: ['dark', 'vast', 'minimal'], description: 'The void of deep space with distant starlight', compatibleMusicMoods: ['ambient', 'calm', 'dreamy'], recommendedFor: ['puzzle', 'story', 'rpg'] },
colors: ['#000005', '#050510', '#0A0A20', '#111133'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
Draw.glow(ctx, w * 0.8, h * 0.15, 150, H.rgba('#1A1A55', 0.3));
},
renderMid: function(ctx, w, h, c, Draw, t) {
Draw.stars(ctx, w, h, 300, '#FFFFFF', 50, [0.2, 1.5]);
Draw.stars(ctx, w, h, 20, '#AABBFF', 51, [1.5, 3]);
var rng = H.seededRandom(300);
for (var i = 0; i < 5; i++) {
var x = rng() * w, y = rng() * h;
var twinkle = 0.3 + Math.sin(t * 1.5 + rng() * 10) * 0.3;
Draw.glow(ctx, x, y, 8 + rng() * 12, H.rgba('#4466CC', twinkle));
}
},
particles: { type: 'stars', count: 60, color: '#FFFFFF' }
},
planets: {
meta: { id: 'space_planets', mood: 'wonder', colors: ['#0B0B2B', '#0F1640', '#CC5533', '#44BBAA'], tags: ['planetary', 'colorful', 'majestic'], description: 'Colorful planets against a starry backdrop', compatibleMusicMoods: ['epic', 'dreamy', 'ambient'], recommendedFor: ['creative', 'rpg', 'story'] },
colors: ['#0B0B2B', '#0F1640', '#CC5533', '#44BBAA'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.stars(ctx, w, h, 150, '#FFFFFF', 40, [0.3, 1.8]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(400);
// Large planet
var px = w * 0.7, py = h * 0.35, pr = 60 + rng() * 40;
var g = Draw.radialGradient(ctx, px - pr * 0.3, py - pr * 0.3, pr * 1.2, [[0, H.lighten(c[2], 0.3)], [0.6, c[2]], [1, H.darken(c[2], 0.6)]]);
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(px, py, pr, 0, Math.PI * 2); ctx.fill();
// Ring
ctx.strokeStyle = H.rgba(c[2], 0.4);
ctx.lineWidth = 3;
ctx.beginPath(); ctx.ellipse(px, py, pr * 1.8, pr * 0.3, -0.2, 0, Math.PI * 2); ctx.stroke();
// Small planet
var sx = w * 0.25, sy = h * 0.6, sr = 25 + rng() * 15;
var g2 = Draw.radialGradient(ctx, sx - sr * 0.3, sy - sr * 0.3, sr * 1.2, [[0, H.lighten(c[3], 0.3)], [0.7, c[3]], [1, H.darken(c[3], 0.5)]]);
ctx.fillStyle = g2;
ctx.beginPath(); ctx.arc(sx, sy, sr, 0, Math.PI * 2); ctx.fill();
// Moon
var bob = Math.sin(t * 0.5) * 5;
var mx = w * 0.15, my = h * 0.25 + bob;
Draw.circle(ctx, mx, my, 10, '#AAAAAA');
Draw.glow(ctx, mx, my, 18, H.rgba('#AAAAAA', 0.2));
},
particles: { type: 'sparkles', count: 30, color: '#FFFFFF' }
},
spaceStation: {
meta: { id: 'space_spaceStation', mood: 'focused', colors: ['#0A0A1E', '#151530', '#3A3A55', '#88AACC'], tags: ['tech', 'structure', 'industrial'], description: 'Orbital station with geometric structures', compatibleMusicMoods: ['ambient', 'tense', 'electronic'], recommendedFor: ['puzzle', 'action', 'physics'] },
colors: ['#0A0A1E', '#151530', '#3A3A55', '#88AACC'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.stars(ctx, w, h, 100, '#FFFFFF', 60, [0.3, 1.2]);
// Earth glow on horizon
Draw.glow(ctx, w * 0.5, h * 1.1, h * 0.5, H.rgba('#2255AA', 0.25));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(500);
// Station core
Draw.rect(ctx, w * 0.35, h * 0.38, w * 0.3, h * 0.06, c[2]);
Draw.rect(ctx, w * 0.33, h * 0.4, w * 0.34, h * 0.02, H.lighten(c[2], 0.1));
// Solar panels
for (var s = 0; s < 2; s++) {
var sx = s === 0 ? w * 0.15 : w * 0.7;
Draw.rect(ctx, sx, h * 0.36, w * 0.14, h * 0.1, H.rgba(c[3], 0.5));
Draw.grid(ctx, w * 0.14, h * 0.1, 10, H.rgba(c[2], 0.3), 0.5);
ctx.save(); ctx.translate(sx, h * 0.36);
Draw.grid(ctx, w * 0.14, h * 0.1, 12, H.rgba('#4466AA', 0.3), 0.5);
ctx.restore();
}
// Struts
Draw.rect(ctx, w * 0.29, h * 0.4, w * 0.04, h * 0.005, c[2]);
Draw.rect(ctx, w * 0.67, h * 0.4, w * 0.04, h * 0.005, c[2]);
// Blinking lights
for (var i = 0; i < 6; i++) {
var lx = w * 0.36 + i * (w * 0.28 / 5);
var blink = Math.sin(t * 3 + i * 1.2) > 0.3 ? 0.9 : 0.15;
Draw.circle(ctx, lx, h * 0.38, 2, H.rgba('#FF3333', blink));
}
},
particles: { type: 'dust', count: 15, color: '#667788' }
},
warpSpeed: {
meta: { id: 'space_warpSpeed', mood: 'energetic', colors: ['#000010', '#0A0A30', '#2244AA', '#66BBFF'], tags: ['fast', 'streaks', 'intense'], description: 'Light streaks from faster-than-light travel', compatibleMusicMoods: ['energetic', 'electronic', 'epic'], recommendedFor: ['racing', 'action', 'sports'] },
colors: ['#000010', '#0A0A30', '#2244AA', '#66BBFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Central tunnel glow
var pulse = 0.6 + Math.sin(t * 2) * 0.15;
Draw.glow(ctx, w * 0.5, h * 0.5, Math.min(w, h) * 0.4 * pulse, c[2]);
Draw.glow(ctx, w * 0.5, h * 0.5, Math.min(w, h) * 0.15, H.rgba(c[3], 0.4));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(600);
var cx = w * 0.5, cy = h * 0.5;
ctx.lineWidth = 1.5;
for (var i = 0; i < 60; i++) {
var angle = rng() * Math.PI * 2;
var dist = 30 + rng() * Math.min(w, h) * 0.45;
var len = 20 + rng() * 60;
var speed = 0.5 + rng() * 2;
var offset = (t * speed * 40 + rng() * 200) % (dist + len);
var x1 = cx + Math.cos(angle) * offset;
var y1 = cy + Math.sin(angle) * offset;
var x2 = cx + Math.cos(angle) * (offset + len);
var y2 = cy + Math.sin(angle) * (offset + len);
var alpha = H.clamp(0.15 + (offset / dist) * 0.6, 0, 0.8);
var col = rng() > 0.7 ? c[3] : '#FFFFFF';
ctx.strokeStyle = H.rgba(col, alpha);
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
}
},
particles: { type: 'particles', count: 30, color: '#88CCFF' }
},
satellites: {
meta: { id: 'space_satellites', mood: 'calm', colors: ['#060615', '#101030', '#334466', '#AACCDD'], tags: ['orbital', 'tech', 'night'], description: 'Satellites orbiting above a dark Earth', compatibleMusicMoods: ['ambient', 'calm', 'dreamy'], recommendedFor: ['puzzle', 'trivia', 'creative'] },
colors: ['#060615', '#101030', '#334466', '#AACCDD'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Earth curve at bottom
var g = Draw.radialGradient(ctx, w * 0.5, h * 1.6, h * 0.9, [[0, '#1A3355'], [0.5, '#0D1B2A'], [1, H.rgba(c[0], 0)]]);
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(w * 0.5, h * 1.6, h * 0.9, 0, Math.PI * 2); ctx.fill();
// Atmosphere glow
Draw.glow(ctx, w * 0.5, h * 1.05, h * 0.3, H.rgba('#3388CC', 0.15));
Draw.stars(ctx, w, h * 0.7, 120, '#FFFFFF', 70, [0.3, 1.5]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(700);
for (var i = 0; i < 5; i++) {
var orbit = 0.15 + rng() * 0.5;
var speed = 0.1 + rng() * 0.25;
var angle = t * speed + rng() * Math.PI * 2;
var ox = w * 0.5 + Math.cos(angle) * w * orbit;
var oy = h * 0.5 + Math.sin(angle) * h * orbit * 0.3;
// Satellite body
Draw.rect(ctx, ox - 4, oy - 2, 8, 4, c[2]);
// Solar panels
Draw.rect(ctx, ox - 14, oy - 1, 9, 2, H.rgba(c[3], 0.7));
Draw.rect(ctx, ox + 5, oy - 1, 9, 2, H.rgba(c[3], 0.7));
// Signal blink
var blink = Math.sin(t * 4 + i * 2) > 0.5 ? 0.8 : 0.1;
Draw.circle(ctx, ox, oy, 1.5, H.rgba('#FF4444', blink));
}
// Orbit lines (faint)
ctx.strokeStyle = H.rgba(c[2], 0.08);
ctx.lineWidth = 0.5;
for (var j = 0; j < 3; j++) {
var orb = 0.2 + j * 0.15;
ctx.beginPath();
ctx.ellipse(w * 0.5, h * 0.5, w * orb, h * orb * 0.3, 0, 0, Math.PI * 2);
ctx.stroke();
}
},
particles: { type: 'stars', count: 40, color: '#FFFFFF' }
},
blackHole: {
meta: { id: 'space_blackHole', mood: 'intense', colors: ['#000000', '#0A0510', '#220A33', '#FF6622'], tags: ['dark', 'gravity', 'dramatic'], description: 'A black hole bending light around its event horizon', compatibleMusicMoods: ['tense', 'epic', 'ambient'], recommendedFor: ['action', 'rpg', 'physics'] },
colors: ['#000000', '#0A0510', '#220A33', '#FF6622'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[0], c[1]], 90);
Draw.stars(ctx, w, h, 80, '#FFFFFF', 80, [0.3, 1]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var cx = w * 0.5, cy = h * 0.45;
var r = Math.min(w, h) * 0.15;
// Accretion disk
var rng = H.seededRandom(800);
ctx.save();
ctx.translate(cx, cy);
for (var ring = 5; ring >= 0; ring--) {
var rr = r * (1.3 + ring * 0.4);
var alpha = 0.08 + ring * 0.04;
var spin = t * (0.2 + ring * 0.05);
ctx.save();
ctx.rotate(spin);
var col = ring < 3 ? c[3] : c[2];
ctx.strokeStyle = H.rgba(col, alpha + Math.sin(t + ring) * 0.03);
ctx.lineWidth = 3 + ring * 1.5;
ctx.beginPath();
ctx.ellipse(0, 0, rr, rr * 0.22, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
// Core glow
Draw.glow(ctx, 0, 0, r * 1.5, H.rgba(c[3], 0.35 + Math.sin(t * 1.5) * 0.1));
Draw.glow(ctx, 0, 0, r * 0.8, H.rgba(c[2], 0.4));
// Black center
var gc = Draw.radialGradient(ctx, 0, 0, r * 0.6, [[0, '#000000'], [0.7, '#000000'], [1, H.rgba('#000000', 0)]]);
ctx.fillStyle = gc;
ctx.beginPath(); ctx.arc(0, 0, r * 0.6, 0, Math.PI * 2); ctx.fill();
ctx.restore();
// Gravitational lensing — bent stars near hole
for (var i = 0; i < 12; i++) {
var a = rng() * Math.PI * 2;
var d = r * (1.1 + rng() * 0.8);
var sx = cx + Math.cos(a + t * 0.15) * d;
var sy = cy + Math.sin(a + t * 0.15) * d * 0.4;
var stretch = 1 + 2 / (1 + d / r);
ctx.fillStyle = H.rgba('#FFCC88', 0.4 + rng() * 0.3);
ctx.beginPath(); ctx.ellipse(sx, sy, 1.5 * stretch, 1, a, 0, Math.PI * 2); ctx.fill();
}
},
particles: { type: 'particles', count: 35, color: '#FF8844' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,694 @@
// spooky.js — Spooky/horror theme for procedural background system
// 8 variants: hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
// Helper: draw a tombstone
function drawTombstone(ctx, x, baseY, w, h, color) {
ctx.fillStyle = color;
// Body
ctx.fillRect(x - w / 2, baseY - h, w, h);
// Rounded top
ctx.beginPath();
ctx.arc(x, baseY - h, w / 2, Math.PI, 0);
ctx.fill();
// Cross or RIP text
ctx.fillStyle = H.darken(color, 0.3);
ctx.fillRect(x - 1, baseY - h + 5, 2, 12);
ctx.fillRect(x - 4, baseY - h + 8, 8, 2);
}
// Helper: draw a bare tree silhouette
function drawBareTree(ctx, x, baseY, height, color, rng) {
var trunkW = height * 0.08;
ctx.fillStyle = color;
// Trunk
ctx.beginPath();
ctx.moveTo(x - trunkW, baseY);
ctx.lineTo(x - trunkW * 0.4, baseY - height * 0.6);
ctx.lineTo(x + trunkW * 0.4, baseY - height * 0.6);
ctx.lineTo(x + trunkW, baseY);
ctx.closePath();
ctx.fill();
// Branches
ctx.strokeStyle = color;
ctx.lineWidth = 2;
var branchCount = 4 + Math.floor(rng() * 4);
for (var i = 0; i < branchCount; i++) {
var by = baseY - height * (0.3 + rng() * 0.5);
var dir = rng() > 0.5 ? 1 : -1;
var bLen = 15 + rng() * height * 0.4;
var bAngle = -Math.PI / 2 + dir * (0.3 + rng() * 0.8);
ctx.beginPath();
ctx.moveTo(x, by);
var bx = x + Math.cos(bAngle) * bLen;
var bby = by + Math.sin(bAngle) * bLen;
ctx.lineTo(bx, bby);
ctx.stroke();
// Sub-branches
if (rng() > 0.3) {
var subLen = bLen * 0.5;
var subAngle = bAngle + (rng() - 0.5) * 0.6;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(bx, bby);
ctx.lineTo(bx + Math.cos(subAngle) * subLen, bby + Math.sin(subAngle) * subLen);
ctx.stroke();
ctx.lineWidth = 2;
}
}
}
// Helper: draw a cobweb
function drawCobweb(ctx, cx, cy, r, color) {
ctx.strokeStyle = color;
ctx.lineWidth = 0.5;
// Radial lines
for (var i = 0; i < 6; i++) {
var a = (i / 6) * Math.PI / 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(a) * r, cy + Math.sin(a) * r);
ctx.stroke();
}
// Spiral rings
for (var ring = 1; ring <= 4; ring++) {
var rr = r * ring / 4;
ctx.beginPath();
for (var i = 0; i <= 6; i++) {
var a = (i / 6) * Math.PI / 2;
var px = cx + Math.cos(a) * rr;
var py = cy + Math.sin(a) * rr;
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.stroke();
}
}
// Helper: draw a cracked wall segment
function drawCracks(ctx, x, y, w, h, color, rng) {
ctx.strokeStyle = color;
ctx.lineWidth = 1;
for (var i = 0; i < 5; i++) {
var sx = x + rng() * w, sy = y + rng() * h;
ctx.beginPath();
ctx.moveTo(sx, sy);
for (var j = 0; j < 3; j++) {
sx += (rng() - 0.5) * 20;
sy += rng() * 15;
ctx.lineTo(sx, sy);
}
ctx.stroke();
}
}
// Helper: draw a moon
function drawMoon(ctx, cx, cy, r, color) {
// Glow
D.glow(ctx, cx, cy, r * 3, H.rgba(color, 0.15));
D.glow(ctx, cx, cy, r * 1.8, H.rgba(color, 0.2));
// Moon face
var g = D.radialGradient(ctx, cx - r * 0.2, cy - r * 0.2, r, [[0, H.lighten(color, 0.3)], [0.7, color], [1, H.darken(color, 0.2)]]);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
}
engine.registerTheme('spooky', {
hauntedHouse: {
meta: { id: 'spooky_hauntedHouse', mood: 'eerie', colors: ['#0A0812', '#1A1025', '#3A2255', '#CCCC88'], tags: ['haunted', 'dark', 'building'], description: 'A looming haunted house silhouette against a moonlit sky', compatibleMusicMoods: ['tense', 'ambient', 'dreamy'], recommendedFor: ['story', 'puzzle', 'rpg'] },
colors: ['#0A0812', '#1A1025', '#3A2255', '#CCCC88'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
// Moon
drawMoon(ctx, w * 0.78, h * 0.15, 30, c[3]);
// Ground
Draw.hills(ctx, w, h, h * 0.82, 5, H.darken(c[0], 0.3), 91);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(100);
// House silhouette (center)
var hx = w * 0.35, hy = h * 0.82;
var hw = w * 0.3, hh = h * 0.35;
ctx.fillStyle = '#0A0808';
// Main body
ctx.fillRect(hx, hy - hh, hw, hh);
// Roof (triangle)
ctx.beginPath();
ctx.moveTo(hx - 10, hy - hh);
ctx.lineTo(hx + hw / 2, hy - hh - h * 0.12);
ctx.lineTo(hx + hw + 10, hy - hh);
ctx.closePath();
ctx.fill();
// Tower on right
ctx.fillRect(hx + hw * 0.7, hy - hh - h * 0.15, hw * 0.2, h * 0.15);
ctx.beginPath();
ctx.moveTo(hx + hw * 0.68, hy - hh - h * 0.15);
ctx.lineTo(hx + hw * 0.8, hy - hh - h * 0.22);
ctx.lineTo(hx + hw * 0.92, hy - hh - h * 0.15);
ctx.closePath();
ctx.fill();
// Windows with flickering light
var windows = [
[hx + hw * 0.15, hy - hh + hh * 0.2],
[hx + hw * 0.55, hy - hh + hh * 0.2],
[hx + hw * 0.15, hy - hh + hh * 0.55],
[hx + hw * 0.55, hy - hh + hh * 0.55],
[hx + hw * 0.76, hy - hh - h * 0.08]
];
for (var i = 0; i < windows.length; i++) {
var flicker = 0.15 + Math.sin(t * 2.5 + rng() * 10) * 0.1 + rng() * 0.1;
var ww = i < 4 ? hw * 0.18 : hw * 0.1;
var wh = i < 4 ? hh * 0.18 : hh * 0.12;
D.glow(ctx, windows[i][0] + ww / 2, windows[i][1] + wh / 2, 15, H.rgba('#DDAA44', flicker * 0.4));
Draw.rect(ctx, windows[i][0], windows[i][1], ww, wh, H.rgba('#DDAA44', flicker));
}
// Bats near moon
for (var i = 0; i < 4; i++) {
var bx = w * 0.65 + rng() * w * 0.25;
var by = h * 0.08 + rng() * h * 0.15;
var wingFlap = Math.sin(t * 6 + i * 2) * 4;
ctx.fillStyle = '#0A0808';
ctx.beginPath();
ctx.ellipse(bx, by, 2, 1, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.quadraticCurveTo(bx - 6, by - wingFlap, bx - 10, by + 2);
ctx.quadraticCurveTo(bx - 6, by + wingFlap * 0.5, bx, by);
ctx.fill();
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.quadraticCurveTo(bx + 6, by - wingFlap, bx + 10, by + 2);
ctx.quadraticCurveTo(bx + 6, by + wingFlap * 0.5, bx, by);
ctx.fill();
}
// Cobweb in corner
drawCobweb(ctx, 0, 0, 60, H.rgba('#AAAAAA', 0.1));
},
particles: { type: 'fog', count: 10, color: '#443366' }
},
graveyard: {
meta: { id: 'spooky_graveyard', mood: 'somber', colors: ['#080810', '#121828', '#2A3348', '#99AA77'], tags: ['cemetery', 'death', 'moonlit'], description: 'Moonlit graveyard with crooked tombstones and mist', compatibleMusicMoods: ['tense', 'ambient', 'dreamy'], recommendedFor: ['rpg', 'story', 'puzzle'] },
colors: ['#080810', '#121828', '#2A3348', '#99AA77'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
drawMoon(ctx, w * 0.82, h * 0.12, 25, '#DDDDAA');
// Ground
Draw.hills(ctx, w, h, h * 0.75, 8, H.darken(c[3], 0.7), 66);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(150);
// Tombstones
for (var i = 0; i < 12; i++) {
var tx = rng() * w * 0.9 + w * 0.05;
var tBaseY = h * 0.75 + rng() * h * 0.08;
var tw = 10 + rng() * 14;
var th = 18 + rng() * 25;
var tilt = (rng() - 0.5) * 0.15;
ctx.save();
ctx.translate(tx, tBaseY);
ctx.rotate(tilt);
var stoneCol = H.blendColor('#444455', '#555566', rng());
drawTombstone(ctx, 0, 0, tw, th, stoneCol);
ctx.restore();
}
// Bare tree silhouette
drawBareTree(ctx, w * 0.15, h * 0.75, h * 0.4, '#0A0A10', rng);
drawBareTree(ctx, w * 0.88, h * 0.76, h * 0.35, '#0A0A10', rng);
// Iron fence
var fenceY = h * 0.78;
ctx.strokeStyle = '#222233';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, fenceY);
ctx.lineTo(w, fenceY);
ctx.stroke();
for (var i = 0; i < w; i += 18) {
ctx.beginPath();
ctx.moveTo(i, fenceY);
ctx.lineTo(i, fenceY - 20);
ctx.stroke();
// Spike top
ctx.beginPath();
ctx.moveTo(i - 3, fenceY - 20);
ctx.lineTo(i, fenceY - 26);
ctx.lineTo(i + 3, fenceY - 20);
ctx.closePath();
ctx.fillStyle = '#222233';
ctx.fill();
}
// Ground fog
for (var i = 0; i < 8; i++) {
var fx = rng() * w;
var fy = h * 0.73 + rng() * h * 0.1;
var drift = Math.sin(t * 0.3 + i) * 15;
ctx.fillStyle = H.rgba('#667788', 0.04 + rng() * 0.03);
ctx.beginPath();
ctx.ellipse(fx + drift, fy, 60 + rng() * 80, 12 + rng() * 8, 0, 0, Math.PI * 2);
ctx.fill();
}
// Ghostly wisp
var ghostX = w * 0.5 + Math.sin(t * 0.6) * 30;
var ghostY = h * 0.45 + Math.cos(t * 0.4) * 10;
D.glow(ctx, ghostX, ghostY, 20, H.rgba('#AABBCC', 0.06 + Math.sin(t * 1.5) * 0.03));
},
particles: { type: 'fog', count: 12, color: '#556677' }
},
darkForest: {
meta: { id: 'spooky_darkForest', mood: 'foreboding', colors: ['#050808', '#0A1510', '#1A2A18', '#3A5530'], tags: ['trees', 'dark', 'wilderness'], description: 'Dense dark forest with twisted trees and glowing eyes', compatibleMusicMoods: ['tense', 'ambient', 'epic'], recommendedFor: ['rpg', 'action', 'story'] },
colors: ['#050808', '#0A1510', '#1A2A18', '#3A5530'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
// Faint moon glow through canopy
D.glow(ctx, w * 0.6, h * 0.1, 100, H.rgba('#AABB88', 0.06));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(200);
// Background tree layer (faint)
for (var i = 0; i < 8; i++) {
drawBareTree(ctx, rng() * w, h * 0.85 + rng() * h * 0.05, h * 0.5 + rng() * h * 0.2, H.rgba(c[2], 0.4), rng);
}
// Foreground tree layer (dark silhouettes)
for (var i = 0; i < 5; i++) {
drawBareTree(ctx, rng() * w, h * 0.9 + rng() * h * 0.05, h * 0.55 + rng() * h * 0.25, '#060A06', rng);
}
// Ground cover
Draw.hills(ctx, w, h, h * 0.88, 12, H.darken(c[2], 0.6), 201);
// Glowing eyes (pairs)
for (var i = 0; i < 4; i++) {
var ex = rng() * w * 0.8 + w * 0.1;
var ey = h * 0.4 + rng() * h * 0.35;
var eyeGlow = 0.3 + Math.sin(t * 1.2 + rng() * 10) * 0.25;
var blink = Math.sin(t * 0.15 + i * 3.7);
if (blink > -0.9) { // eyes open most of the time
D.circle(ctx, ex - 4, ey, 2, H.rgba('#FFDD44', eyeGlow));
D.circle(ctx, ex + 4, ey, 2, H.rgba('#FFDD44', eyeGlow));
D.glow(ctx, ex, ey, 10, H.rgba('#FFDD44', eyeGlow * 0.2));
}
}
// Mushrooms on ground
for (var i = 0; i < 6; i++) {
var mx = rng() * w;
var my = h * 0.88 + rng() * h * 0.06;
var mr = 3 + rng() * 4;
// Stem
Draw.rect(ctx, mx - 1, my - mr, 2, mr, H.rgba('#887766', 0.4));
// Cap
ctx.fillStyle = H.rgba(c[3], 0.3 + Math.sin(t * 0.8 + i) * 0.1);
ctx.beginPath();
ctx.arc(mx, my - mr, mr, Math.PI, 0);
ctx.fill();
// Faint bioluminescence
D.glow(ctx, mx, my - mr, mr * 2, H.rgba(c[3], 0.04));
}
// Hanging moss strands
for (var i = 0; i < 10; i++) {
var mx = rng() * w;
var my = rng() * h * 0.3;
var mLen = 15 + rng() * 30;
var sway = Math.sin(t * 0.5 + i * 1.1) * 3;
ctx.strokeStyle = H.rgba(c[3], 0.12);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(mx, my);
ctx.quadraticCurveTo(mx + sway, my + mLen / 2, mx + sway * 0.5, my + mLen);
ctx.stroke();
}
},
particles: { type: 'fireflies', count: 12, color: '#88CC44' }
},
abandonedBuilding: {
meta: { id: 'spooky_abandonedBuilding', mood: 'desolate', colors: ['#0A0A0E', '#181820', '#3A3A48', '#667766'], tags: ['ruins', 'urban', 'decay'], description: 'Crumbling abandoned building with broken windows and debris', compatibleMusicMoods: ['tense', 'ambient', 'electronic'], recommendedFor: ['action', 'puzzle', 'rpg'] },
colors: ['#0A0A0E', '#181820', '#3A3A48', '#667766'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Dim sky glow
D.glow(ctx, w * 0.5, h * 0.05, w * 0.4, H.rgba('#223344', 0.1));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(250);
// Building facade
var bx = w * 0.15, bw = w * 0.7, bh = h * 0.65;
var by = h * 0.9 - bh;
Draw.rect(ctx, bx, by, bw, bh, c[2]);
// Cracked texture
drawCracks(ctx, bx, by, bw, bh, H.rgba('#222230', 0.4), rng);
// Windows (some broken, some dark, some flickering)
var cols = 5, rows = 4;
var winW = bw * 0.1, winH = bh * 0.12;
for (var r = 0; r < rows; r++) {
for (var col = 0; col < cols; col++) {
var wx = bx + bw * 0.08 + col * (bw * 0.18);
var wy = by + bh * 0.08 + r * (bh * 0.22);
var state = rng(); // 0-0.4 dark, 0.4-0.7 broken, 0.7-1 dim light
if (state < 0.4) {
Draw.rect(ctx, wx, wy, winW, winH, H.darken(c[2], 0.4));
} else if (state < 0.7) {
// Broken window — jagged frame
Draw.rect(ctx, wx, wy, winW, winH, '#0A0A0E');
ctx.strokeStyle = H.rgba(c[2], 0.5);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(wx + winW * 0.3, wy);
ctx.lineTo(wx + winW * 0.5, wy + winH * 0.6);
ctx.lineTo(wx + winW * 0.7, wy + winH * 0.2);
ctx.stroke();
} else {
var flicker = 0.08 + Math.sin(t * 3 + rng() * 10) * 0.05;
Draw.rect(ctx, wx, wy, winW, winH, H.rgba('#DDAA55', flicker));
}
}
}
// Ground debris
Draw.rect(ctx, 0, h * 0.9, w, h * 0.1, H.darken(c[2], 0.5));
for (var i = 0; i < 10; i++) {
var dx = rng() * w;
var dy = h * 0.9 + rng() * h * 0.05;
var dw = 5 + rng() * 15;
var dh = 3 + rng() * 8;
Draw.rect(ctx, dx, dy, dw, dh, H.blendColor(c[2], c[3], rng() * 0.3));
}
// Cobweb in top corner of building
drawCobweb(ctx, bx, by, 40, H.rgba('#888888', 0.08));
drawCobweb(ctx, bx + bw, by, 40, H.rgba('#888888', 0.08));
// Flickering overhead light
var lightBlink = Math.sin(t * 8) > 0.3 ? 0.5 : (Math.sin(t * 12) > 0.8 ? 0.3 : 0);
if (lightBlink > 0) {
D.glow(ctx, w * 0.5, by - 5, 40, H.rgba('#FFDDAA', lightBlink * 0.15));
}
},
particles: { type: 'dust', count: 20, color: '#666655' }
},
crypt: {
meta: { id: 'spooky_crypt', mood: 'dread', colors: ['#060608', '#101015', '#2A2A35', '#558855'], tags: ['underground', 'stone', 'ancient'], description: 'Underground stone crypt with torchlight and coffins', compatibleMusicMoods: ['tense', 'ambient', 'epic'], recommendedFor: ['rpg', 'action', 'story'] },
colors: ['#060608', '#101015', '#2A2A35', '#558855'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[0], c[1]], 90);
// Stone walls
Draw.rect(ctx, 0, 0, w, h * 0.15, c[2]);
Draw.rect(ctx, 0, h * 0.85, w, h * 0.15, H.darken(c[2], 0.3));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(300);
// Stone block pattern on ceiling and floor
for (var row = 0; row < 2; row++) {
var sy = row === 0 ? 0 : h * 0.85;
var sh = h * 0.15;
ctx.strokeStyle = H.rgba(c[0], 0.5);
ctx.lineWidth = 1;
var brickH = sh / 3;
for (var r = 0; r < 3; r++) {
var offset = r % 2 === 0 ? 0 : 25;
for (var bx = -25 + offset; bx < w; bx += 50) {
ctx.strokeRect(bx, sy + r * brickH, 50, brickH);
}
}
}
// Arched columns
for (var i = 0; i < 4; i++) {
var cx = w * 0.1 + i * w * 0.27;
// Column
Draw.rect(ctx, cx - 8, h * 0.15, 16, h * 0.7, H.darken(c[2], 0.15));
Draw.rect(ctx, cx - 10, h * 0.15, 20, 8, c[2]);
Draw.rect(ctx, cx - 10, h * 0.82, 20, 8, c[2]);
// Arch between columns
if (i < 3) {
var nextCx = cx + w * 0.27;
ctx.strokeStyle = H.rgba(c[2], 0.3);
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc((cx + nextCx) / 2, h * 0.15, (nextCx - cx) / 2, Math.PI, 0);
ctx.stroke();
}
}
// Torches on columns
for (var i = 0; i < 3; i++) {
var tx = w * 0.1 + i * w * 0.27 + w * 0.135;
var ty = h * 0.3;
// Bracket
Draw.rect(ctx, tx - 2, ty, 4, 12, '#554433');
// Flame
var flameFlicker = 0.6 + Math.sin(t * 5 + i * 2) * 0.2 + Math.sin(t * 8 + i) * 0.1;
D.glow(ctx, tx, ty, 40, H.rgba('#FF8833', flameFlicker * 0.12));
D.glow(ctx, tx, ty - 3, 8, H.rgba('#FFCC44', flameFlicker * 0.6));
ctx.fillStyle = H.rgba('#FF8833', flameFlicker);
ctx.beginPath();
ctx.moveTo(tx - 4, ty);
ctx.quadraticCurveTo(tx, ty - 12 - Math.sin(t * 6 + i) * 3, tx + 4, ty);
ctx.fill();
}
// Coffins on floor
for (var i = 0; i < 2; i++) {
var cofX = w * 0.25 + i * w * 0.35;
var cofY = h * 0.78;
ctx.fillStyle = '#1A1510';
ctx.beginPath();
ctx.moveTo(cofX - 10, cofY);
ctx.lineTo(cofX - 15, cofY + 5);
ctx.lineTo(cofX - 12, cofY + 35);
ctx.lineTo(cofX + 12, cofY + 35);
ctx.lineTo(cofX + 15, cofY + 5);
ctx.lineTo(cofX + 10, cofY);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = H.rgba(c[3], 0.15);
ctx.lineWidth = 0.5;
ctx.stroke();
}
// Dripping water
for (var i = 0; i < 3; i++) {
var dx = rng() * w;
var dropY = ((t * 35 + rng() * 200) % (h * 0.7)) + h * 0.15;
var alpha = 1 - (dropY - h * 0.15) / (h * 0.7);
D.circle(ctx, dx, dropY, 1.5, H.rgba('#5577AA', alpha * 0.3));
}
// Green mold/moss patches
for (var i = 0; i < 6; i++) {
var mx = rng() * w;
var my = rng() > 0.5 ? h * 0.12 + rng() * h * 0.05 : h * 0.83 + rng() * h * 0.05;
ctx.fillStyle = H.rgba(c[3], 0.08 + rng() * 0.05);
ctx.beginPath();
ctx.ellipse(mx, my, 10 + rng() * 20, 4 + rng() * 6, 0, 0, Math.PI * 2);
ctx.fill();
}
},
particles: { type: 'dust', count: 15, color: '#554433' }
},
manor: {
meta: { id: 'spooky_manor', mood: 'gothic', colors: ['#0A080C', '#1A1020', '#3A2040', '#886644'], tags: ['elegant', 'dark', 'victorian'], description: 'Gothic manor interior with candlelight and portraits', compatibleMusicMoods: ['ambient', 'tense', 'dreamy'], recommendedFor: ['story', 'rpg', 'puzzle'] },
colors: ['#0A080C', '#1A1020', '#3A2040', '#886644'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Wallpaper pattern (subtle)
var rng = H.seededRandom(351);
for (var y = 0; y < h; y += 40) {
for (var x = 0; x < w; x += 40) {
D.polygon(ctx, x + 20, y + 20, 8, 4, H.rgba(c[2], 0.06), rng() * 0.5);
}
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(350);
// Wainscoting
Draw.rect(ctx, 0, h * 0.65, w, h * 0.35, H.darken(c[3], 0.6));
// Panel lines
for (var i = 0; i < 6; i++) {
var px = w * 0.05 + i * w * 0.16;
ctx.strokeStyle = H.rgba(c[3], 0.15);
ctx.lineWidth = 1;
ctx.strokeRect(px, h * 0.68, w * 0.12, h * 0.25);
}
// Floor
Draw.rect(ctx, 0, h * 0.92, w, h * 0.08, H.darken(c[3], 0.7));
// Portrait frames on wall
for (var i = 0; i < 3; i++) {
var fx = w * 0.15 + i * w * 0.3;
var fy = h * 0.2;
var fw = w * 0.15, fh = h * 0.25;
// Frame
ctx.strokeStyle = c[3];
ctx.lineWidth = 3;
ctx.strokeRect(fx, fy, fw, fh);
// Dark canvas inside
Draw.rect(ctx, fx + 3, fy + 3, fw - 6, fh - 6, H.darken(c[2], 0.4));
// Hint of a face (oval)
ctx.fillStyle = H.rgba(c[3], 0.06);
ctx.beginPath();
ctx.ellipse(fx + fw / 2, fy + fh * 0.4, fw * 0.2, fh * 0.25, 0, 0, Math.PI * 2);
ctx.fill();
// Eyes that follow (subtle)
var eyeShift = Math.sin(t * 0.2 + i) * 1.5;
D.circle(ctx, fx + fw * 0.4 + eyeShift, fy + fh * 0.38, 1, H.rgba('#FFFFFF', 0.08));
D.circle(ctx, fx + fw * 0.6 + eyeShift, fy + fh * 0.38, 1, H.rgba('#FFFFFF', 0.08));
}
// Candelabra
for (var i = 0; i < 2; i++) {
var cx = w * 0.3 + i * w * 0.4;
var cy = h * 0.52;
// Base
Draw.rect(ctx, cx - 2, cy, 4, 15, c[3]);
// Arms
for (var a = -1; a <= 1; a++) {
var armX = cx + a * 15;
ctx.strokeStyle = c[3];
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx, cy + 3);
ctx.quadraticCurveTo(cx + a * 8, cy - 5, armX, cy - 8);
ctx.stroke();
// Candle
Draw.rect(ctx, armX - 2, cy - 18, 4, 10, '#E8DDD0');
// Flame
var ff = 0.5 + Math.sin(t * 4 + i + a) * 0.2;
D.glow(ctx, armX, cy - 20, 12, H.rgba('#FF9944', ff * 0.2));
ctx.fillStyle = H.rgba('#FFCC55', ff);
ctx.beginPath();
ctx.ellipse(armX, cy - 22, 2, 4 + Math.sin(t * 6 + a) * 1, 0, 0, Math.PI * 2);
ctx.fill();
}
}
// Cobweb top corners
drawCobweb(ctx, 0, 0, 50, H.rgba('#999999', 0.06));
drawCobweb(ctx, w, 0, 50, H.rgba('#999999', 0.06));
},
particles: { type: 'dust', count: 12, color: '#665544' }
},
fog: {
meta: { id: 'spooky_fog', mood: 'mysterious', colors: ['#0A0E12', '#151E28', '#2A3844', '#556688'], tags: ['misty', 'atmospheric', 'obscured'], description: 'Thick rolling fog obscuring dark shapes and dim lights', compatibleMusicMoods: ['ambient', 'dreamy', 'tense'], recommendedFor: ['story', 'puzzle', 'rpg'] },
colors: ['#0A0E12', '#151E28', '#2A3844', '#556688'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90);
// Diffused moon glow
D.glow(ctx, w * 0.5, h * 0.08, 120, H.rgba('#AABBCC', 0.08));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(420);
// Background shapes barely visible through fog
for (var i = 0; i < 4; i++) {
var sx = rng() * w;
var sh = 40 + rng() * 80;
Draw.rect(ctx, sx - 15, h * 0.75 - sh, 30, sh, H.rgba(c[1], 0.2));
}
// Bare tree hints
ctx.strokeStyle = H.rgba(c[1], 0.15);
ctx.lineWidth = 3;
for (var i = 0; i < 3; i++) {
var tx = rng() * w;
ctx.beginPath();
ctx.moveTo(tx, h * 0.85);
ctx.lineTo(tx - 3, h * 0.5);
ctx.stroke();
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(tx - 1, h * 0.6);
ctx.lineTo(tx - 20, h * 0.45);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(tx + 1, h * 0.55);
ctx.lineTo(tx + 18, h * 0.42);
ctx.stroke();
ctx.lineWidth = 3;
}
// Ground
Draw.hills(ctx, w, h, h * 0.85, 6, H.rgba(c[1], 0.5), 421);
// Multi-layer fog bands
for (var layer = 0; layer < 5; layer++) {
var fogY = h * 0.3 + layer * h * 0.13;
var drift = Math.sin(t * 0.15 + layer * 1.2) * 40;
for (var i = 0; i < 5; i++) {
var fx = rng() * w * 1.4 - w * 0.2 + drift;
var fw = 100 + rng() * 200;
var fh = 20 + rng() * 40;
var alpha = 0.04 + rng() * 0.04;
ctx.fillStyle = H.rgba(c[3], alpha);
ctx.beginPath();
ctx.ellipse(fx, fogY, fw, fh, 0, 0, Math.PI * 2);
ctx.fill();
}
}
// Distant dim lights through fog
for (var i = 0; i < 3; i++) {
var lx = rng() * w;
var ly = h * 0.5 + rng() * h * 0.25;
var pulse = 0.03 + Math.sin(t * 0.8 + rng() * 10) * 0.02;
D.glow(ctx, lx, ly, 25, H.rgba('#FFDDAA', pulse));
}
},
particles: { type: 'fog', count: 18, color: '#667788' }
},
shadows: {
meta: { id: 'spooky_shadows', mood: 'paranoid', colors: ['#050506', '#0E0E14', '#1C1C2A', '#332244'], tags: ['dark', 'minimal', 'threatening'], description: 'Near-total darkness with shifting shadows and faint light', compatibleMusicMoods: ['tense', 'ambient', 'electronic'], recommendedFor: ['action', 'puzzle', 'rpg'] },
colors: ['#050506', '#0E0E14', '#1C1C2A', '#332244'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(500);
// Central dim light source
var lightPulse = 0.08 + Math.sin(t * 0.6) * 0.03;
D.glow(ctx, w * 0.5, h * 0.5, Math.min(w, h) * 0.35, H.rgba('#443355', lightPulse));
// Shadow blobs that shift and drift
for (var i = 0; i < 8; i++) {
var sx = rng() * w;
var sy = rng() * h;
var sr = 40 + rng() * 100;
var driftX = Math.sin(t * 0.2 + i * 1.5) * 20;
var driftY = Math.cos(t * 0.15 + i * 1.8) * 15;
var shadowAlpha = 0.15 + Math.sin(t * 0.3 + rng() * 10) * 0.08;
ctx.fillStyle = H.rgba(c[0], shadowAlpha);
ctx.beginPath();
ctx.ellipse(sx + driftX, sy + driftY, sr, sr * 0.6, rng() * Math.PI, 0, Math.PI * 2);
ctx.fill();
}
// Faint scratch-like lines
ctx.strokeStyle = H.rgba(c[3], 0.04);
ctx.lineWidth = 0.5;
for (var i = 0; i < 12; i++) {
var lx = rng() * w;
var ly = rng() * h;
var lLen = 30 + rng() * 60;
var lAngle = rng() * Math.PI;
ctx.beginPath();
ctx.moveTo(lx, ly);
ctx.lineTo(lx + Math.cos(lAngle) * lLen, ly + Math.sin(lAngle) * lLen);
ctx.stroke();
}
// Occasional flash of something
var flashTimer = Math.sin(t * 0.08);
if (flashTimer > 0.97) {
var fx = rng() * w * 0.6 + w * 0.2;
var fy = rng() * h * 0.6 + h * 0.2;
D.glow(ctx, fx, fy, 50, H.rgba('#554466', 0.15));
}
// Edge vignette (darker edges)
var vig = D.radialGradient(ctx, w / 2, h / 2, Math.max(w, h) * 0.6, [[0, H.rgba(c[0], 0)], [0.6, H.rgba(c[0], 0.3)], [1, H.rgba(c[0], 0.7)]]);
ctx.fillStyle = vig;
ctx.fillRect(0, 0, w, h);
// Two faint eyes in the dark
var eyeAppear = Math.sin(t * 0.12);
if (eyeAppear > 0.3) {
var ea = (eyeAppear - 0.3) / 0.7;
var ex = w * 0.65 + Math.sin(t * 0.08) * 20;
var ey = h * 0.4;
D.circle(ctx, ex - 6, ey, 2, H.rgba('#CC4444', ea * 0.3));
D.circle(ctx, ex + 6, ey, 2, H.rgba('#CC4444', ea * 0.3));
}
},
particles: { type: 'dust', count: 8, color: '#333344' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,595 @@
// sports.js — Sports venue background themes
// Variants: stadium, raceTrack, court, field, arena, pool, gym, skatepark
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('sports', {
stadium: {
meta: { id: 'sports_stadium', mood: 'exciting', colors: ['#1a3a1a', '#2a5a2a', '#88cc44', '#ffffff'], tags: ['stadium', 'football', 'soccer', 'crowd'], description: 'Large outdoor stadium with green pitch, bright floodlights, and packed stands', compatibleMusicMoods: ['exciting', 'energetic', 'upbeat'], recommendedFor: ['sports', 'action', 'trivia'] },
colors: ['#1a3a1a', '#2a5a2a', '#88cc44', '#ffffff'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Night sky
Draw.fillGradient(ctx, w, h, ['#0a0a1e', '#1a1a2e'], 90);
// Pitch
Draw.rect(ctx, w * 0.08, h * 0.4, w * 0.84, h * 0.45, c[1]);
// Mowed stripes on pitch
for (var s = 0; s < 8; s++) {
var sx = w * 0.08 + s * w * 0.105;
Draw.rect(ctx, sx, h * 0.4, w * 0.105, h * 0.45, s % 2 === 0 ? c[1] : H.lighten(c[1], 0.08));
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1001);
// Field markings
ctx.strokeStyle = H.rgba(c[3], 0.6); ctx.lineWidth = 2;
ctx.strokeRect(w * 0.1, h * 0.42, w * 0.8, h * 0.41);
// Center line
ctx.beginPath(); ctx.moveTo(w * 0.5, h * 0.42); ctx.lineTo(w * 0.5, h * 0.83); ctx.stroke();
// Center circle
ctx.beginPath(); ctx.arc(w * 0.5, h * 0.625, h * 0.08, 0, Math.PI * 2); ctx.stroke();
Draw.circle(ctx, w * 0.5, h * 0.625, 3, H.rgba(c[3], 0.6));
// Goal boxes
ctx.strokeRect(w * 0.1, h * 0.52, w * 0.08, h * 0.21);
ctx.strokeRect(w * 0.82, h * 0.52, w * 0.08, h * 0.21);
// Stands
for (var row = 0; row < 5; row++) {
var ry = h * 0.08 + row * h * 0.06;
Draw.rect(ctx, w * 0.02, ry, w * 0.96, h * 0.05, H.rgba('#444466', 0.6 - row * 0.08));
// Crowd dots
for (var c2 = 0; c2 < 40; c2++) {
var cx = w * 0.04 + rng() * w * 0.92;
var crowdColor = H.pick(['#cc3333', '#3333cc', '#ffffff', '#ffcc00', '#33cc33', '#ff6600']);
var bob = Math.sin(t * 2 + c2 * 0.3 + row) * 1;
Draw.circle(ctx, cx, ry + h * 0.025 + bob, 2, H.rgba(crowdColor, 0.5));
}
}
// Bottom stands
for (var row2 = 0; row2 < 3; row2++) {
var ry2 = h * 0.87 + row2 * h * 0.04;
Draw.rect(ctx, w * 0.02, ry2, w * 0.96, h * 0.035, H.rgba('#444466', 0.5 - row2 * 0.1));
for (var c3 = 0; c3 < 30; c3++) {
var cx2 = w * 0.04 + rng() * w * 0.92;
Draw.circle(ctx, cx2, ry2 + h * 0.017, 2, H.rgba(H.pick(['#cc3333', '#3333cc', '#ffffff', '#ffcc00']), 0.4));
}
}
// Floodlights
for (var fl = 0; fl < 4; fl++) {
var fx = w * 0.05 + fl * w * 0.3;
Draw.rect(ctx, fx, h * 0.0, 4, h * 0.12, '#666');
Draw.rect(ctx, fx - 8, 0, 20, 8, '#888');
var lightPulse = 0.15 + Math.sin(t * 0.3 + fl * 0.5) * 0.03;
Draw.glow(ctx, fx + 2, h * 0.02, 50, H.rgba('#FFFFFF', lightPulse));
// Light cones on pitch
ctx.fillStyle = H.rgba('#FFFFFF', 0.02 + Math.sin(t * 0.2 + fl) * 0.005);
ctx.beginPath(); ctx.moveTo(fx, h * 0.08); ctx.lineTo(fx - w * 0.15, h * 0.85);
ctx.lineTo(fx + w * 0.15, h * 0.85); ctx.closePath(); ctx.fill();
}
},
particles: { type: 'sparkles', count: 12, color: '#FFFFFF' }
},
raceTrack: {
meta: { id: 'sports_racetrack', mood: 'fast', colors: ['#333333', '#555555', '#cc0000', '#ffffff'], tags: ['racing', 'speed', 'track', 'cars'], description: 'Asphalt race track with lane markings, kerbing, and grandstand', compatibleMusicMoods: ['energetic', 'fast', 'intense'], recommendedFor: ['racing', 'action', 'sports'] },
colors: ['#333333', '#555555', '#cc0000', '#ffffff'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Sky
Draw.fillGradient(ctx, w, h, ['#4488bb', '#88bbdd'], 90);
// Green surroundings
Draw.rect(ctx, 0, h * 0.3, w, h * 0.7, '#3a6a2a');
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1101);
// Track surface (oval perspective)
ctx.fillStyle = c[0];
ctx.beginPath();
ctx.moveTo(w * 0.05, h * 0.85); ctx.lineTo(w * 0.2, h * 0.4);
ctx.lineTo(w * 0.8, h * 0.4); ctx.lineTo(w * 0.95, h * 0.85);
ctx.closePath(); ctx.fill();
// Inner grass
ctx.fillStyle = '#4a7a3a';
ctx.beginPath();
ctx.moveTo(w * 0.2, h * 0.82); ctx.lineTo(w * 0.3, h * 0.48);
ctx.lineTo(w * 0.7, h * 0.48); ctx.lineTo(w * 0.8, h * 0.82);
ctx.closePath(); ctx.fill();
// Lane markings
ctx.strokeStyle = H.rgba(c[3], 0.6); ctx.lineWidth = 2; ctx.setLineDash([15, 15]);
for (var l = 1; l < 4; l++) {
var t1 = l / 4;
ctx.beginPath();
ctx.moveTo(H.lerp(w * 0.05, w * 0.2, t1), H.lerp(h * 0.85, h * 0.82, t1));
ctx.lineTo(H.lerp(w * 0.2, w * 0.3, t1), H.lerp(h * 0.4, h * 0.48, t1));
ctx.lineTo(H.lerp(w * 0.8, w * 0.7, t1), H.lerp(h * 0.4, h * 0.48, t1));
ctx.lineTo(H.lerp(w * 0.95, w * 0.8, t1), H.lerp(h * 0.85, h * 0.82, t1));
ctx.stroke();
}
ctx.setLineDash([]);
// Kerbing (red-white stripes)
for (var k = 0; k < 20; k++) {
var kx1 = H.lerp(w * 0.2, w * 0.8, k / 20);
var kx2 = H.lerp(w * 0.2, w * 0.8, (k + 1) / 20);
Draw.rect(ctx, kx1, h * 0.39, kx2 - kx1, h * 0.02, k % 2 === 0 ? c[2] : c[3]);
}
// Start/finish line
for (var sq = 0; sq < 10; sq++) {
for (var sr = 0; sr < 3; sr++) {
var checker = (sq + sr) % 2 === 0;
Draw.rect(ctx, w * 0.48 + sq * 4, h * 0.65 + sr * 4, 4, 4, checker ? '#000' : '#fff');
}
}
// Grandstand
Draw.rect(ctx, w * 0.3, h * 0.25, w * 0.4, h * 0.12, '#556677');
for (var gr = 0; gr < 4; gr++) {
var gy = h * 0.26 + gr * h * 0.025;
for (var gs = 0; gs < 25; gs++) {
var gx = w * 0.32 + rng() * w * 0.36;
Draw.circle(ctx, gx, gy + h * 0.012, 1.5, H.rgba(H.pick(['#ff3333', '#3355cc', '#ffcc00', '#ffffff']), 0.5));
}
}
// Clouds
Draw.clouds(ctx, w, h, h * 0.08, 4, '#FFFFFF', 1102);
// Tire marks with animation
var skidOffset = (t * 40) % w;
ctx.strokeStyle = H.rgba('#222', 0.15); ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(w * 0.35, h * 0.75);
ctx.quadraticCurveTo(w * 0.45, h * 0.7, w * 0.55, h * 0.72); ctx.stroke();
},
particles: { type: 'dust', count: 15, color: '#AAAAAA' }
},
court: {
meta: { id: 'sports_court', mood: 'competitive', colors: ['#c47a34', '#e8944a', '#ffffff', '#2255aa'], tags: ['basketball', 'court', 'indoor', 'wood'], description: 'Indoor basketball court with polished hardwood, court lines, and arena lighting', compatibleMusicMoods: ['energetic', 'upbeat', 'competitive'], recommendedFor: ['sports', 'action', 'trivia'] },
colors: ['#c47a34', '#e8944a', '#ffffff', '#2255aa'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Arena dark upper
Draw.fillGradient(ctx, w, h, ['#1a1a2a', '#2a2a3a'], 90);
// Hardwood floor
var floorY = h * 0.35;
Draw.rect(ctx, w * 0.03, floorY, w * 0.94, h * 0.55, c[0]);
// Wood grain lines
var rng = H.seededRandom(1201);
for (var g = 0; g < 20; g++) {
var gx = w * 0.03 + g * (w * 0.94 / 20);
ctx.strokeStyle = H.rgba(H.darken(c[0], 0.15), 0.3); ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(gx, floorY); ctx.lineTo(gx, floorY + h * 0.55); ctx.stroke();
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1202);
var floorY = h * 0.35;
// Court lines
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
ctx.strokeRect(w * 0.08, floorY + h * 0.03, w * 0.84, h * 0.49);
// Center line
ctx.beginPath(); ctx.moveTo(w * 0.5, floorY + h * 0.03); ctx.lineTo(w * 0.5, floorY + h * 0.52); ctx.stroke();
// Center circle
ctx.beginPath(); ctx.arc(w * 0.5, floorY + h * 0.275, h * 0.08, 0, Math.PI * 2); ctx.stroke();
// Three-point arcs
ctx.beginPath(); ctx.arc(w * 0.1, floorY + h * 0.275, h * 0.2, -0.9, 0.9); ctx.stroke();
ctx.beginPath(); ctx.arc(w * 0.9, floorY + h * 0.275, h * 0.2, Math.PI - 0.9, Math.PI + 0.9); ctx.stroke();
// Free throw lanes (paint)
ctx.fillStyle = H.rgba(c[3], 0.15);
ctx.fillRect(w * 0.08, floorY + h * 0.14, w * 0.12, h * 0.27);
ctx.fillRect(w * 0.8, floorY + h * 0.14, w * 0.12, h * 0.27);
ctx.strokeRect(w * 0.08, floorY + h * 0.14, w * 0.12, h * 0.27);
ctx.strokeRect(w * 0.8, floorY + h * 0.14, w * 0.12, h * 0.27);
// Backboards and hoops
for (var side = 0; side < 2; side++) {
var hx = side === 0 ? w * 0.09 : w * 0.91;
Draw.rect(ctx, hx - 8, floorY + h * 0.2, 16, h * 0.06, '#FFFFFF');
ctx.strokeStyle = '#FF4400'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(hx, floorY + h * 0.275, 8, 0, Math.PI * 2); ctx.stroke();
}
// Scoreboard
Draw.rect(ctx, w * 0.35, h * 0.04, w * 0.3, h * 0.08, '#111');
var scoreGlow = 0.7 + Math.sin(t * 0.5) * 0.15;
ctx.fillStyle = H.rgba('#ff3333', scoreGlow);
ctx.font = 'bold 14px monospace';
ctx.fillText('HOME', w * 0.38, h * 0.08);
ctx.fillText('AWAY', w * 0.55, h * 0.08);
ctx.fillStyle = H.rgba('#00ff44', scoreGlow);
ctx.fillText('88', w * 0.42, h * 0.1);
ctx.fillText('85', w * 0.58, h * 0.1);
// Arena lights
for (var lt = 0; lt < 6; lt++) {
var lx = w * 0.1 + lt * w * 0.16;
Draw.rect(ctx, lx, h * 0.13, w * 0.06, 4, '#ddd');
var intensity = 0.06 + Math.sin(t * 0.2 + lt * 0.4) * 0.015;
Draw.glow(ctx, lx + w * 0.03, h * 0.14, 40, H.rgba('#FFFFFF', intensity));
}
// Crowd in upper stands
for (var cr = 0; cr < 3; cr++) {
var cry = h * 0.15 + cr * h * 0.06;
for (var ci = 0; ci < 25; ci++) {
var ccx = w * 0.06 + rng() * w * 0.88;
Draw.circle(ctx, ccx, cry, 1.5, H.rgba(H.pick(['#cc3333', '#3355cc', '#ffffff', '#ffaa00']), 0.35));
}
}
// Floor reflection shimmer
var shimmer = 0.03 + Math.sin(t * 0.4) * 0.01;
ctx.fillStyle = H.rgba('#FFFFFF', shimmer);
ctx.fillRect(w * 0.03, floorY, w * 0.94, h * 0.55);
},
particles: { type: 'sparkles', count: 8, color: '#FFFFFF' }
},
field: {
meta: { id: 'sports_field', mood: 'fresh', colors: ['#2a7a2a', '#3a9a3a', '#ffffff', '#88cc44'], tags: ['soccer', 'field', 'outdoor', 'grass'], description: 'Open soccer field under blue sky with goals, corner flags, and freshly cut grass', compatibleMusicMoods: ['upbeat', 'cheerful', 'energetic'], recommendedFor: ['sports', 'action', 'pet'] },
colors: ['#2a7a2a', '#3a9a3a', '#ffffff', '#88cc44'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Sky
Draw.fillGradient(ctx, w, h, ['#3388cc', '#88ccee', '#aaddee'], 90);
// Field with mowed stripes
for (var s = 0; s < 10; s++) {
var sx = s * (w / 10);
Draw.rect(ctx, sx, h * 0.45, w / 10, h * 0.55, s % 2 === 0 ? c[0] : c[1]);
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1301);
// Field markings
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
// Boundary
ctx.strokeRect(w * 0.05, h * 0.47, w * 0.9, h * 0.48);
// Halfway line
ctx.beginPath(); ctx.moveTo(w * 0.5, h * 0.47); ctx.lineTo(w * 0.5, h * 0.95); ctx.stroke();
// Center circle
ctx.beginPath(); ctx.arc(w * 0.5, h * 0.71, h * 0.08, 0, Math.PI * 2); ctx.stroke();
Draw.circle(ctx, w * 0.5, h * 0.71, 3, c[2]);
// Penalty areas
ctx.strokeRect(w * 0.05, h * 0.57, w * 0.1, h * 0.28);
ctx.strokeRect(w * 0.85, h * 0.57, w * 0.1, h * 0.28);
// Goal areas
ctx.strokeRect(w * 0.05, h * 0.63, w * 0.04, h * 0.16);
ctx.strokeRect(w * 0.91, h * 0.63, w * 0.04, h * 0.16);
// Goals (nets)
for (var g = 0; g < 2; g++) {
var gx = g === 0 ? w * 0.02 : w * 0.95;
var gw = w * 0.03;
Draw.rect(ctx, gx, h * 0.63, gw, h * 0.16, H.rgba('#FFFFFF', 0.15));
ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 2;
ctx.strokeRect(gx, h * 0.63, gw, h * 0.16);
// Net mesh
ctx.strokeStyle = H.rgba('#FFFFFF', 0.2); ctx.lineWidth = 0.5;
for (var n = 0; n < 5; n++) {
ctx.beginPath(); ctx.moveTo(gx, h * 0.63 + n * h * 0.032); ctx.lineTo(gx + gw, h * 0.63 + n * h * 0.032); ctx.stroke();
ctx.beginPath(); ctx.moveTo(gx + n * gw / 5, h * 0.63); ctx.lineTo(gx + n * gw / 5, h * 0.79); ctx.stroke();
}
}
// Corner flags
for (var cf = 0; cf < 4; cf++) {
var cfx = cf < 2 ? w * 0.05 : w * 0.95;
var cfy = cf % 2 === 0 ? h * 0.47 : h * 0.95;
Draw.rect(ctx, cfx - 1, cfy - 16, 2, 16, '#888');
var flagWave = Math.sin(t * 3 + cf) * 3;
ctx.fillStyle = '#ff4444';
ctx.beginPath(); ctx.moveTo(cfx, cfy - 16); ctx.lineTo(cfx + 8 + flagWave, cfy - 13);
ctx.lineTo(cfx, cfy - 10); ctx.closePath(); ctx.fill();
}
// Clouds
Draw.clouds(ctx, w, h, h * 0.08, 5, '#FFFFFF', 1302);
// Sun
var sunPulse = 0.8 + Math.sin(t * 0.2) * 0.1;
Draw.circle(ctx, w * 0.85, h * 0.1, 20, H.rgba('#ffdd44', sunPulse));
Draw.glow(ctx, w * 0.85, h * 0.1, 50, '#ffdd44');
// Trees along side
Draw.trees(ctx, w, h, h * 0.45, 8, '#5a3a1a', '#2a6a2a', 1303);
},
particles: { type: 'leaves', count: 8, color: '#88CC44' }
},
arena: {
meta: { id: 'sports_arena', mood: 'intense', colors: ['#1a1a2e', '#2a2a44', '#ff4444', '#ffaa00'], tags: ['arena', 'boxing', 'mma', 'spotlight'], description: 'Dark fighting arena with dramatic spotlights, ropes, and smoky atmosphere', compatibleMusicMoods: ['intense', 'dramatic', 'energetic'], recommendedFor: ['action', 'sports', 'rpg'] },
colors: ['#1a1a2e', '#2a2a44', '#ff4444', '#ffaa00'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Dark floor
Draw.rect(ctx, 0, h * 0.7, w, h * 0.3, H.darken(c[0], 0.3));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1401);
// Ring platform
ctx.fillStyle = '#333344';
ctx.beginPath();
ctx.moveTo(w * 0.15, h * 0.7); ctx.lineTo(w * 0.25, h * 0.5);
ctx.lineTo(w * 0.75, h * 0.5); ctx.lineTo(w * 0.85, h * 0.7);
ctx.closePath(); ctx.fill();
// Ring canvas (mat)
ctx.fillStyle = '#e8e0d0';
ctx.beginPath();
ctx.moveTo(w * 0.22, h * 0.52); ctx.lineTo(w * 0.27, h * 0.48);
ctx.lineTo(w * 0.73, h * 0.48); ctx.lineTo(w * 0.78, h * 0.52);
ctx.closePath(); ctx.fill();
// Ring mat surface
ctx.fillStyle = H.rgba(c[2], 0.08);
ctx.beginPath();
ctx.moveTo(w * 0.22, h * 0.52); ctx.lineTo(w * 0.27, h * 0.48);
ctx.lineTo(w * 0.73, h * 0.48); ctx.lineTo(w * 0.78, h * 0.52);
ctx.closePath(); ctx.fill();
// Corner posts
var posts = [[w * 0.27, h * 0.48], [w * 0.73, h * 0.48], [w * 0.22, h * 0.52], [w * 0.78, h * 0.52]];
for (var p = 0; p < 4; p++) {
Draw.rect(ctx, posts[p][0] - 3, posts[p][1] - h * 0.12, 6, h * 0.12, '#888');
// Turnbuckle
Draw.circle(ctx, posts[p][0], posts[p][1] - h * 0.12, 4, p < 2 ? c[2] : c[3]);
}
// Ropes
for (var r = 0; r < 3; r++) {
var ropeY = h * 0.48 - (r + 1) * h * 0.03;
var sag = Math.sin(t * 0.5 + r) * 2;
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
// Top ropes
ctx.beginPath(); ctx.moveTo(w * 0.27, ropeY);
ctx.quadraticCurveTo(w * 0.5, ropeY + sag + 3, w * 0.73, ropeY); ctx.stroke();
// Side ropes (perspective)
var sideRopeY = ropeY + h * 0.02;
ctx.beginPath(); ctx.moveTo(w * 0.27, ropeY);
ctx.quadraticCurveTo(w * 0.245, sideRopeY + sag, w * 0.22, sideRopeY + h * 0.04); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w * 0.73, ropeY);
ctx.quadraticCurveTo(w * 0.755, sideRopeY + sag, w * 0.78, sideRopeY + h * 0.04); ctx.stroke();
}
// Spotlights
for (var sp = 0; sp < 3; sp++) {
var spx = w * 0.3 + sp * w * 0.2;
var pulse = 0.12 + Math.sin(t * 0.6 + sp * 1.5) * 0.04;
Draw.glow(ctx, spx, 0, 30, H.rgba(c[3], 0.4));
ctx.fillStyle = H.rgba(c[3], pulse);
ctx.beginPath(); ctx.moveTo(spx - 15, 0); ctx.lineTo(spx + 15, 0);
ctx.lineTo(spx + w * 0.1, h * 0.7); ctx.lineTo(spx - w * 0.1, h * 0.7);
ctx.closePath(); ctx.fill();
}
// Crowd silhouettes
for (var cr = 0; cr < 50; cr++) {
var ccx = rng() * w;
var ccy = h * 0.7 + rng() * h * 0.15;
var bob = Math.sin(t * 1.5 + cr * 0.5) * 1.5;
Draw.circle(ctx, ccx, ccy + bob, 3 + rng() * 2, H.rgba('#333344', 0.7));
}
// Smoke effect
for (var sm = 0; sm < 4; sm++) {
var smx = w * 0.3 + rng() * w * 0.4 + Math.sin(t * 0.2 + sm) * 15;
var smy = h * 0.3 + rng() * h * 0.15;
ctx.fillStyle = H.rgba('#aaaacc', 0.03);
ctx.beginPath(); ctx.ellipse(smx, smy, 60 + rng() * 40, 20, 0, 0, Math.PI * 2); ctx.fill();
}
},
particles: { type: 'dust', count: 20, color: '#CCBBAA' }
},
pool: {
meta: { id: 'sports_pool', mood: 'calm', colors: ['#1a6088', '#2288aa', '#44bbdd', '#88eeff'], tags: ['swimming', 'water', 'pool', 'blue'], description: 'Olympic swimming pool with lane dividers, rippling water, and tiled edges', compatibleMusicMoods: ['calm', 'peaceful', 'ambient'], recommendedFor: ['sports', 'racing', 'puzzle'] },
colors: ['#1a6088', '#2288aa', '#44bbdd', '#88eeff'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Indoor ceiling
Draw.fillGradient(ctx, w, h, ['#e8e4dc', '#d8d4cc'], 90);
// Pool water
var waterStart = h * 0.35;
ctx.fillStyle = Draw.linearGradient(ctx, 0, waterStart, 0, h, [
[0, c[1]], [0.5, c[2]], [1, c[0]]
]);
ctx.fillRect(w * 0.05, waterStart, w * 0.9, h * 0.55);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1501);
var waterStart = h * 0.35;
var poolW = w * 0.9;
var poolX = w * 0.05;
// Lane dividers
var lanes = 8;
for (var l = 1; l < lanes; l++) {
var lx = poolX + l * (poolW / lanes);
ctx.strokeStyle = H.rgba('#FFFFFF', 0.3); ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(lx, waterStart); ctx.lineTo(lx, waterStart + h * 0.55); ctx.stroke();
// Lane rope beads
for (var b = 0; b < 15; b++) {
var by = waterStart + b * (h * 0.55 / 15);
var beadColor = b % 3 === 0 ? c[3] : (b % 3 === 1 ? '#ff4444' : '#ffffff');
Draw.circle(ctx, lx, by, 2, H.rgba(beadColor, 0.5));
}
}
// Water caustics (animated light patterns)
for (var ca = 0; ca < 25; ca++) {
var cx = poolX + rng() * poolW;
var cy = waterStart + rng() * h * 0.5;
var caSize = 15 + rng() * 30;
var caAlpha = 0.03 + Math.sin(t * 1.5 + ca * 0.7) * 0.02;
ctx.fillStyle = H.rgba(c[3], caAlpha);
ctx.beginPath();
ctx.moveTo(cx, cy - caSize * 0.3);
ctx.quadraticCurveTo(cx + caSize * 0.4, cy, cx, cy + caSize * 0.3);
ctx.quadraticCurveTo(cx - caSize * 0.4, cy, cx, cy - caSize * 0.3);
ctx.fill();
}
// Water surface ripples
for (var rp = 0; rp < 12; rp++) {
var ry = waterStart + rp * 2;
ctx.strokeStyle = H.rgba('#FFFFFF', 0.06);
ctx.lineWidth = 0.5;
ctx.beginPath();
for (var rx = poolX; rx < poolX + poolW; rx += 3) {
ctx.lineTo(rx, ry + Math.sin(rx * 0.03 + t * 2 + rp * 0.5) * 2);
}
ctx.stroke();
}
// Pool edge tiles
Draw.rect(ctx, poolX - 5, waterStart - 5, poolW + 10, 8, '#ddd');
Draw.rect(ctx, poolX - 5, waterStart + h * 0.55 - 3, poolW + 10, 8, '#ddd');
// Tile pattern on edge
for (var ti = 0; ti < 30; ti++) {
var tix = poolX + ti * (poolW / 30);
ctx.strokeStyle = H.rgba('#bbb', 0.4); ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(tix, waterStart - 5); ctx.lineTo(tix, waterStart + 3); ctx.stroke();
}
// Starting blocks
for (var sb = 0; sb < lanes; sb++) {
var sbx = poolX + sb * (poolW / lanes) + poolW / lanes / 2 - 8;
Draw.rect(ctx, sbx, waterStart - 12, 16, 8, '#888');
Draw.rect(ctx, sbx + 2, waterStart - 16, 12, 5, '#aaa');
}
// Lane numbers on bottom
ctx.font = '12px monospace'; ctx.fillStyle = H.rgba('#000', 0.15);
for (var ln = 0; ln < lanes; ln++) {
var lnx = poolX + ln * (poolW / lanes) + poolW / lanes / 2 - 3;
ctx.fillText(String(ln + 1), lnx, waterStart + h * 0.5);
}
// Ceiling lights reflected in water
for (var cl = 0; cl < 5; cl++) {
var clx = w * 0.15 + cl * w * 0.18;
Draw.rect(ctx, clx, h * 0.05, w * 0.08, 4, '#eee');
var refAlpha = 0.04 + Math.sin(t * 0.4 + cl) * 0.02;
Draw.glow(ctx, clx + w * 0.04, waterStart + h * 0.2, 40, H.rgba(c[3], refAlpha));
}
},
particles: { type: 'sparkles', count: 15, color: '#88EEFF' }
},
gym: {
meta: { id: 'sports_gym', mood: 'competitive', colors: ['#ddc8a0', '#c4a870', '#ffffff', '#cc3333'], tags: ['gymnastics', 'gym', 'mats', 'indoor'], description: 'Gymnastics arena with spring floor, apparatus silhouettes, and competition banners', compatibleMusicMoods: ['upbeat', 'focused', 'energetic'], recommendedFor: ['sports', 'action', 'creative'] },
colors: ['#ddc8a0', '#c4a870', '#ffffff', '#cc3333'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Arena interior
Draw.fillGradient(ctx, w, h, ['#2a2a3a', '#3a3a4a'], 90);
// Spring floor
Draw.rect(ctx, w * 0.1, h * 0.6, w * 0.8, h * 0.3, c[0]);
Draw.rect(ctx, w * 0.1, h * 0.6, w * 0.8, h * 0.3, H.rgba(c[2], 0.05));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1601);
// Floor exercise boundary
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
ctx.strokeRect(w * 0.12, h * 0.62, w * 0.76, h * 0.26);
// Diagonal line on floor
ctx.strokeStyle = H.rgba(c[3], 0.2); ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(w * 0.12, h * 0.62); ctx.lineTo(w * 0.88, h * 0.88); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w * 0.88, h * 0.62); ctx.lineTo(w * 0.12, h * 0.88); ctx.stroke();
// Balance beam
Draw.rect(ctx, w * 0.03, h * 0.55, w * 0.18, h * 0.015, c[1]);
Draw.rect(ctx, w * 0.06, h * 0.565, 4, h * 0.12, '#666');
Draw.rect(ctx, w * 0.17, h * 0.565, 4, h * 0.12, '#666');
// Vault table
Draw.rect(ctx, w * 0.78, h * 0.54, w * 0.08, h * 0.03, '#888');
Draw.rect(ctx, w * 0.8, h * 0.57, 4, h * 0.1, '#666');
Draw.rect(ctx, w * 0.84, h * 0.57, 4, h * 0.1, '#666');
// Runway mat
Draw.rect(ctx, w * 0.88, h * 0.56, w * 0.1, h * 0.015, '#4488dd');
// Rings (hanging)
for (var ring = 0; ring < 2; ring++) {
var rx = w * 0.4 + ring * w * 0.06;
ctx.strokeStyle = '#888'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(rx, 0); ctx.lineTo(rx, h * 0.25); ctx.stroke();
var sway = Math.sin(t * 0.3 + ring * Math.PI) * 3;
ctx.strokeStyle = c[1]; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(rx + sway, h * 0.28, 8, 0, Math.PI * 2); ctx.stroke();
}
// Parallel bars
Draw.rect(ctx, w * 0.55, h * 0.45, w * 0.15, 3, '#aa8855');
Draw.rect(ctx, w * 0.55, h * 0.5, w * 0.15, 3, '#aa8855');
Draw.rect(ctx, w * 0.58, h * 0.5, 3, h * 0.17, '#777');
Draw.rect(ctx, w * 0.67, h * 0.5, 3, h * 0.17, '#777');
// Scoreboard
Draw.rect(ctx, w * 0.3, h * 0.05, w * 0.4, h * 0.08, '#111');
ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
ctx.strokeRect(w * 0.3, h * 0.05, w * 0.4, h * 0.08);
var scoreGlow = 0.7 + Math.sin(t * 0.6) * 0.2;
ctx.fillStyle = H.rgba('#00ff66', scoreGlow);
ctx.font = 'bold 12px monospace';
ctx.fillText('FLOOR 9.450', w * 0.35, h * 0.09);
// Competition banners
for (var bn = 0; bn < 5; bn++) {
var bx = w * 0.1 + bn * w * 0.18;
var bannerColor = H.pick([c[3], '#3355cc', '#ffcc00', '#33aa55', '#aa33aa']);
Draw.rect(ctx, bx, h * 0.14, 18, h * 0.08, bannerColor);
// Banner point
ctx.fillStyle = bannerColor;
ctx.beginPath(); ctx.moveTo(bx, h * 0.22); ctx.lineTo(bx + 9, h * 0.24); ctx.lineTo(bx + 18, h * 0.22); ctx.closePath(); ctx.fill();
}
// Arena lights
for (var al = 0; al < 4; al++) {
var alx = w * 0.15 + al * w * 0.22;
var lightAlpha = 0.06 + Math.sin(t * 0.25 + al * 0.6) * 0.015;
Draw.glow(ctx, alx, h * 0.14, 35, H.rgba('#FFFFFF', lightAlpha));
}
},
particles: { type: 'dust', count: 10, color: '#DDCC99' }
},
skatepark: {
meta: { id: 'sports_skatepark', mood: 'cool', colors: ['#555566', '#777788', '#ff6600', '#ffcc00'], tags: ['skate', 'urban', 'concrete', 'graffiti'], description: 'Urban skatepark with concrete ramps, half-pipes, graffiti, and warm sunset light', compatibleMusicMoods: ['cool', 'energetic', 'retro', 'upbeat'], recommendedFor: ['action', 'sports', 'racing', 'creative'] },
colors: ['#555566', '#777788', '#ff6600', '#ffcc00'],
renderBase: function(ctx, w, h, c, Draw, t) {
// Warm sunset sky
Draw.fillGradient(ctx, w, h, ['#dd6633', '#ee9955', '#ffcc88'], 90);
// Concrete ground
Draw.rect(ctx, 0, h * 0.55, w, h * 0.45, c[0]);
// Concrete texture
var rng = H.seededRandom(1700);
for (var sp = 0; sp < 30; sp++) {
Draw.circle(ctx, rng() * w, h * 0.55 + rng() * h * 0.45, 1, H.rgba('#444', 0.15));
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(1701);
// Half-pipe
ctx.fillStyle = c[1];
ctx.beginPath();
ctx.moveTo(w * 0.05, h * 0.55); ctx.quadraticCurveTo(w * 0.05, h * 0.9, w * 0.2, h * 0.9);
ctx.lineTo(w * 0.2, h * 0.55); ctx.closePath(); ctx.fill();
// Coping on half-pipe
Draw.rect(ctx, w * 0.04, h * 0.54, w * 0.17, 3, '#999');
// Quarter pipe on right
ctx.fillStyle = c[1];
ctx.beginPath();
ctx.moveTo(w * 0.75, h * 0.9); ctx.quadraticCurveTo(w * 0.95, h * 0.9, w * 0.95, h * 0.6);
ctx.lineTo(w * 0.95, h * 0.55); ctx.lineTo(w * 0.75, h * 0.55); ctx.closePath(); ctx.fill();
Draw.rect(ctx, w * 0.74, h * 0.54, w * 0.22, 3, '#999');
// Fun box / pyramid
ctx.fillStyle = H.darken(c[0], 0.1);
ctx.beginPath();
ctx.moveTo(w * 0.35, h * 0.85); ctx.lineTo(w * 0.4, h * 0.72);
ctx.lineTo(w * 0.6, h * 0.72); ctx.lineTo(w * 0.65, h * 0.85);
ctx.closePath(); ctx.fill();
// Rail on top
Draw.rect(ctx, w * 0.4, h * 0.715, w * 0.2, 3, '#aaa');
// Grind rail
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(w * 0.25, h * 0.8); ctx.lineTo(w * 0.4, h * 0.8); ctx.stroke();
Draw.rect(ctx, w * 0.25, h * 0.8, 3, h * 0.1, '#888');
Draw.rect(ctx, w * 0.4, h * 0.8, 3, h * 0.1, '#888');
// Graffiti on walls/ramps
var graffitiColors = ['#ff3366', '#33ccff', '#ffcc00', '#44ff44', '#ff6600', '#cc44ff'];
// Back wall
Draw.rect(ctx, 0, h * 0.2, w, h * 0.35, H.rgba(c[0], 0.5));
// Graffiti blobs
for (var g = 0; g < 8; g++) {
var gx = rng() * w * 0.9 + w * 0.05;
var gy = h * 0.25 + rng() * h * 0.22;
var gc = graffitiColors[Math.floor(rng() * graffitiColors.length)];
ctx.fillStyle = H.rgba(gc, 0.2 + rng() * 0.15);
ctx.beginPath();
ctx.ellipse(gx, gy, 15 + rng() * 25, 8 + rng() * 12, rng() * Math.PI, 0, Math.PI * 2);
ctx.fill();
}
// Street light
Draw.rect(ctx, w * 0.7, h * 0.15, 3, h * 0.4, '#666');
Draw.rect(ctx, w * 0.68, h * 0.14, 20, 6, '#777');
var lampGlow = 0.3 + Math.sin(t * 0.4) * 0.1;
Draw.circle(ctx, w * 0.69, h * 0.17, 5, H.rgba(c[3], lampGlow));
Draw.glow(ctx, w * 0.69, h * 0.17, 40, H.rgba(c[3], lampGlow * 0.5));
// Fence in background
for (var f = 0; f < 15; f++) {
var fx = w * 0.02 + f * w * 0.07;
Draw.rect(ctx, fx, h * 0.35, 2, h * 0.2, H.rgba('#888', 0.3));
}
ctx.strokeStyle = H.rgba('#888', 0.25); ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, h * 0.4); ctx.lineTo(w, h * 0.4); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, h * 0.48); ctx.lineTo(w, h * 0.48); ctx.stroke();
// Warm sun glow
Draw.glow(ctx, w * 0.15, h * 0.08, 60, '#ffaa44');
Draw.circle(ctx, w * 0.15, h * 0.08, 18, H.rgba('#ffcc66', 0.8));
},
particles: { type: 'leaves', count: 10, color: '#CC8833' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,267 @@
// underwater.js — Underwater background theme (8 variants)
// Depends on backgroundEngine.js being loaded first
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
function drawFish(ctx, x, y, s, col, dir) {
ctx.fillStyle = col;
ctx.beginPath(); ctx.ellipse(x, y, s, s * 0.5, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.moveTo(x - dir * s, y); ctx.lineTo(x - dir * s * 1.6, y - s * 0.5); ctx.lineTo(x - dir * s * 1.6, y + s * 0.5); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#FFF'; ctx.beginPath(); ctx.arc(x + dir * s * 0.4, y - s * 0.15, s * 0.18, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(x + dir * s * 0.45, y - s * 0.15, s * 0.08, 0, Math.PI * 2); ctx.fill();
}
function sunRays(ctx, w, h, n, col, t) {
ctx.save(); ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < n; i++) {
var rx = (w / n) * i + Math.sin(t * 0.3 + i) * 20, rw = 15 + Math.sin(t * 0.5 + i * 1.3) * 8;
ctx.fillStyle = H.rgba(col, 0.03 + Math.sin(t * 0.4 + i * 0.7) * 0.02);
ctx.beginPath(); ctx.moveTo(rx - rw, 0); ctx.lineTo(rx + rw, 0); ctx.lineTo(rx + rw * 2.5, h); ctx.lineTo(rx - rw * 1.5, h); ctx.closePath(); ctx.fill();
}
ctx.restore();
}
engine.registerTheme('underwater', {
coralReef: {
meta: { id: 'underwater_coralReef', mood: 'cheerful', colors: ['#006994', '#00A8CC', '#FF6F61', '#FFB347'], tags: ['coral', 'reef', 'colorful', 'ocean'], description: 'A vibrant coral reef teeming with colorful marine life', compatibleMusicMoods: ['happy', 'upbeat', 'peaceful'], recommendedFor: ['puzzle', 'pet', 'creative'] },
colors: ['#006994', '#00A8CC', '#FF6F61', '#FFB347'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [H.lighten(c[0], 0.15), c[0], H.darken(c[0], 0.3)], 90);
sunRays(ctx, w, h, 6, '#88DDFF', t);
Draw.rect(ctx, 0, h * 0.82, w, h * 0.18, '#C2A366');
Draw.hills(ctx, w, h, h * 0.82, 10, '#B89555', 22);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(50);
for (var i = 0; i < 14; i++) {
var cx = rng() * w, cy = h * 0.72 + rng() * h * 0.15, ch = 15 + rng() * 35;
var cc = [c[2], c[3], '#FF8899', '#FF77AA'][Math.floor(rng() * 4)];
for (var b = 0; b < 3 + Math.floor(rng() * 3); b++) {
var a = -Math.PI / 2 + (rng() - 0.5) * 1.2, bL = ch * (0.5 + rng() * 0.5), sw = Math.sin(t * 0.8 + i + b) * 2;
ctx.strokeStyle = cc; ctx.lineWidth = 3 + rng() * 4; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.quadraticCurveTo(cx + Math.cos(a) * bL * 0.5 + sw, cy + Math.sin(a) * bL * 0.5, cx + Math.cos(a) * bL + sw, cy + Math.sin(a) * bL); ctx.stroke();
Draw.circle(ctx, cx + Math.cos(a) * bL + sw, cy + Math.sin(a) * bL, 3 + rng() * 3, H.lighten(cc, 0.2));
}
}
for (var f = 0; f < 10; f++) {
var fx = (rng() * w + t * 20 * (0.5 + rng())) % (w + 40) - 20, fy = h * 0.15 + rng() * h * 0.55 + Math.sin(t + f * 2) * 8;
drawFish(ctx, fx, fy, 5 + rng() * 8, [c[2], c[3], '#FFDD44', '#44DDFF'][Math.floor(rng() * 4)], 1);
}
},
particles: { type: 'bubbles', count: 30, color: '#88DDFF' }
},
deepSea: {
meta: { id: 'underwater_deepSea', mood: 'mysterious', colors: ['#0A0A2E', '#0D1B3E', '#1A3A5C', '#00FFCC'], tags: ['deep', 'dark', 'bioluminescent', 'abyss'], description: 'The mysterious deep sea with bioluminescent creatures and faint light', compatibleMusicMoods: ['mysterious', 'ambient', 'dark'], recommendedFor: ['rpg', 'story', 'puzzle'] },
colors: ['#0A0A2E', '#0D1B3E', '#1A3A5C', '#00FFCC'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[2], c[1], c[0]], 90);
Draw.rect(ctx, 0, h * 0.88, w, h * 0.12, H.darken(c[0], 0.3));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(71);
for (var j = 0; j < 6; j++) {
var jx = rng() * w, jy = h * 0.2 + rng() * h * 0.5 + Math.sin(t * 0.6 + j * 1.5) * 15;
var jr = 8 + rng() * 14, jC = rng() > 0.5 ? c[3] : '#BB77FF', p = 0.4 + Math.sin(t * 1.5 + j * 2) * 0.2;
Draw.glow(ctx, jx, jy, jr * 3, H.rgba(jC, p * 0.3));
ctx.fillStyle = H.rgba(jC, p); ctx.beginPath(); ctx.arc(jx, jy, jr, Math.PI, 0); ctx.closePath(); ctx.fill();
ctx.strokeStyle = H.rgba(jC, p * 0.6); ctx.lineWidth = 1;
for (var tt = 0; tt < 5; tt++) {
var tx = jx - jr + (tt / 4) * jr * 2; ctx.beginPath(); ctx.moveTo(tx, jy);
for (var tp = 0; tp < jr * 2 + rng() * jr; tp += 4) ctx.lineTo(tx + Math.sin(tp * 0.3 + t * 2 + tt) * 4, jy + tp);
ctx.stroke();
}
}
for (var a = 0; a < 4; a++) {
var ax = rng() * w, ay = h * 0.6 + rng() * h * 0.25, gl = 0.3 + Math.sin(t * 2.5 + a * 3) * 0.25;
Draw.glow(ctx, ax, ay, 12, H.rgba(c[3], gl)); Draw.circle(ctx, ax, ay, 2, H.rgba('#FFF', gl + 0.2));
}
for (var r = 0; r < 10; r++) Draw.circle(ctx, rng() * w, h * 0.88 + rng() * h * 0.05, 5 + rng() * 15, H.darken(c[1], 0.2 + rng() * 0.15));
},
particles: { type: 'fireflies', count: 20, color: '#00FFCC' }
},
kelpForest: {
meta: { id: 'underwater_kelpForest', mood: 'peaceful', colors: ['#0B4F3A', '#1A7A5C', '#2FA87A', '#7EC8A0'], tags: ['kelp', 'forest', 'green', 'serene'], description: 'A towering kelp forest with dappled light filtering through fronds', compatibleMusicMoods: ['peaceful', 'calm', 'ambient'], recommendedFor: ['puzzle', 'creative', 'story'] },
colors: ['#0B4F3A', '#1A7A5C', '#2FA87A', '#7EC8A0'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [H.lighten(c[0], 0.25), c[0], H.darken(c[0], 0.2)], 90);
sunRays(ctx, w, h, 5, '#AAFFCC', t);
Draw.rect(ctx, 0, h * 0.88, w, h * 0.12, H.darken(c[0], 0.35));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(38);
for (var i = 0; i < 18; i++) {
var kx = rng() * w, kB = h * 0.9, kT = h * 0.05 + rng() * h * 0.3;
var kC = [c[1], c[2], c[3]][Math.floor(rng() * 3)];
ctx.strokeStyle = H.rgba(kC, 0.6 + rng() * 0.3); ctx.lineWidth = 3 + rng() * 3;
ctx.beginPath(); ctx.moveTo(kx, kB);
var seg = 8 + Math.floor(rng() * 6);
for (var s = 1; s <= seg; s++) ctx.lineTo(kx + Math.sin(s * 0.8 + t * 0.7 + i) * (12 + rng() * 8), kB - (kB - kT) * (s / seg));
ctx.stroke();
for (var lf = 0; lf < 4; lf++) {
var lfY = kB - (kB - kT) * ((lf + 1) / 5), d = rng() > 0.5 ? 1 : -1;
ctx.fillStyle = H.rgba(kC, 0.4);
ctx.beginPath(); ctx.ellipse(kx + Math.sin((lf + 1) * 0.8 + t * 0.7 + i) * 12 + d * 8, lfY, 10, 3, d * 0.3, 0, Math.PI * 2); ctx.fill();
}
}
for (var f = 0; f < 7; f++) drawFish(ctx, (rng() * w + t * 25 * (rng() + 0.3)) % (w + 30) - 15, h * 0.2 + rng() * h * 0.5 + Math.sin(t * 2 + f) * 6, 4 + rng() * 4, H.rgba(c[3], 0.7), 1);
},
particles: { type: 'bubbles', count: 20, color: '#88EEBB' }
},
submarine: {
meta: { id: 'underwater_submarine', mood: 'adventurous', colors: ['#0A1628', '#1A3050', '#D4AA44', '#CC4444'], tags: ['submarine', 'exploration', 'industrial', 'adventure'], description: 'A submarine expedition through dark ocean depths with spotlight beams', compatibleMusicMoods: ['adventurous', 'mysterious', 'tense'], recommendedFor: ['action', 'rpg', 'story'] },
colors: ['#0A1628', '#1A3050', '#D4AA44', '#CC4444'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[1], c[0]], 90);
Draw.rect(ctx, 0, h * 0.85, w, h * 0.15, H.darken(c[0], 0.4));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(82), sx = w * 0.5 + Math.sin(t * 0.3) * 20, sy = h * 0.4 + Math.sin(t * 0.5) * 10;
ctx.save(); ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = H.rgba(c[2], 0.06); ctx.beginPath(); ctx.moveTo(sx + 55, sy + 5); ctx.lineTo(sx + 200, sy - 40); ctx.lineTo(sx + 200, sy + 80); ctx.closePath(); ctx.fill();
ctx.restore();
ctx.fillStyle = '#556677'; ctx.beginPath(); ctx.ellipse(sx, sy, 55, 18, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#667788'; ctx.fillRect(sx - 10, sy - 28, 20, 12);
ctx.beginPath(); ctx.arc(sx, sy - 28, 10, Math.PI, 0); ctx.fill();
ctx.strokeStyle = '#778899'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(sx, sy - 28); ctx.lineTo(sx, sy - 42); ctx.lineTo(sx + 5, sy - 42); ctx.stroke();
Draw.circle(ctx, sx + 20, sy, 5, c[2]); Draw.circle(ctx, sx + 20, sy, 3, H.rgba('#FFEE88', 0.6));
ctx.save(); ctx.translate(sx - 55, sy);
for (var p = 0; p < 3; p++) { var pa = t * 4 + p * Math.PI * 2 / 3; ctx.fillStyle = '#8899AA'; ctx.beginPath(); ctx.ellipse(Math.cos(pa) * 6, Math.sin(pa) * 6, 8, 2, pa, 0, Math.PI * 2); ctx.fill(); }
ctx.restore();
for (var r = 0; r < 12; r++) Draw.circle(ctx, rng() * w, h * 0.85 + rng() * h * 0.08, 4 + rng() * 10, H.darken(c[0], 0.15 + rng() * 0.15));
},
particles: { type: 'bubbles', count: 25, color: '#AACCDD' }
},
shipwreck: {
meta: { id: 'underwater_shipwreck', mood: 'mysterious', colors: ['#0A1A2A', '#1A3344', '#8B7355', '#44AA88'], tags: ['shipwreck', 'treasure', 'mysterious', 'old'], description: 'A sunken ship resting on the ocean floor, overgrown with sea life', compatibleMusicMoods: ['mysterious', 'adventurous', 'dark'], recommendedFor: ['rpg', 'story', 'puzzle'] },
colors: ['#0A1A2A', '#1A3344', '#8B7355', '#44AA88'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[1], c[0]], 90);
sunRays(ctx, w, h, 4, '#66BBAA', t);
Draw.hills(ctx, w, h, h * 0.78, 8, '#5A4A3A', 55);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(93), sx = w * 0.45, sy = h * 0.62;
ctx.fillStyle = c[2]; ctx.save(); ctx.translate(sx, sy); ctx.rotate(0.15);
ctx.fillRect(-60, -10, 120, 25); ctx.fillRect(-50, -25, 100, 18);
ctx.fillStyle = H.darken(c[2], 0.2); ctx.fillRect(-40, -35, 80, 12);
ctx.strokeStyle = c[2]; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(-10, -35); ctx.lineTo(-20, -80); ctx.stroke();
ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-20, -60); ctx.lineTo(15, -55); ctx.stroke();
ctx.fillStyle = H.darken(c[0], 0.3);
ctx.beginPath(); ctx.ellipse(20, 0, 8, 5, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(-30, -5, 6, 4, 0.2, 0, Math.PI * 2); ctx.fill();
ctx.restore();
for (var s = 0; s < 8; s++) {
var swx = sx - 50 + rng() * 100; ctx.strokeStyle = H.rgba(c[3], 0.5 + rng() * 0.3); ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(swx, sy + 5);
for (var sp = 0; sp < 15 + rng() * 25; sp += 3) ctx.lineTo(swx + Math.sin(sp * 0.4 + t * 1.2 + s) * 5, sy + 5 - sp);
ctx.stroke();
}
for (var f = 0; f < 5; f++) drawFish(ctx, (rng() * w + t * 12 * (rng() + 0.2)) % (w + 30) - 15, h * 0.3 + rng() * h * 0.35 + Math.sin(t + f) * 5, 4 + rng() * 5, H.rgba(c[3], 0.6), 1);
},
particles: { type: 'bubbles', count: 20, color: '#66AAAA' }
},
abyss: {
meta: { id: 'underwater_abyss', mood: 'dark', colors: ['#020208', '#050515', '#0A0A30', '#FF2200'], tags: ['abyss', 'dark', 'deep', 'volcanic'], description: 'The abyssal zone with volcanic vents and glowing magma cracks', compatibleMusicMoods: ['dark', 'tense', 'ambient'], recommendedFor: ['action', 'rpg', 'story'] },
colors: ['#020208', '#050515', '#0A0A30', '#FF2200'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[2], c[1], c[0]], 90);
Draw.hills(ctx, w, h, h * 0.8, 6, '#0D0D12', 80);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(13);
for (var v = 0; v < 3; v++) {
var vx = w * 0.2 + rng() * w * 0.6, vy = h * 0.82, vg = 0.3 + Math.sin(t * 2 + v * 3) * 0.15;
ctx.fillStyle = '#1A1510'; ctx.fillRect(vx - 8, vy - 20, 16, 25); ctx.fillRect(vx - 12, vy - 22, 24, 5);
Draw.glow(ctx, vx, vy - 22, 25, H.rgba(c[3], vg));
for (var sm = 0; sm < 6; sm++) {
ctx.fillStyle = H.rgba(c[3], 0.15 * (1 - sm / 6));
ctx.beginPath(); ctx.arc(vx + Math.sin(t * 1.5 + sm * 2 + v) * 8, vy - 25 - sm * 18 - (t * 10 % 30), 5 + sm * 2, 0, Math.PI * 2); ctx.fill();
}
}
for (var lc = 0; lc < 8; lc++) {
var lcx = rng() * w, lcy = h * 0.83 + rng() * h * 0.1, g2 = 0.2 + Math.sin(t * 1.8 + lc) * 0.15;
Draw.glow(ctx, lcx, lcy, 10, H.rgba(c[3], g2 * 0.4));
ctx.strokeStyle = H.rgba(c[3], g2); ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(lcx, lcy); ctx.lineTo(lcx + (rng() - 0.5) * 30, lcy + (rng() - 0.5) * 8); ctx.stroke();
}
for (var s = 0; s < 12; s++) Draw.circle(ctx, rng() * w, rng() * h * 0.75, 1 + rng(), H.rgba('#4488FF', 0.15 + Math.sin(t * 3 + s * 2.3) * 0.1));
},
particles: { type: 'particles', count: 15, color: '#FF4400' }
},
iceCave: {
meta: { id: 'underwater_iceCave', mood: 'calm', colors: ['#0A2A4A', '#1A5A8A', '#88CCEE', '#E0F0FF'], tags: ['ice', 'cave', 'arctic', 'frozen'], description: 'An underwater ice cavern with crystalline formations and pale blue light', compatibleMusicMoods: ['calm', 'ambient', 'mysterious'], recommendedFor: ['puzzle', 'story', 'creative'] },
colors: ['#0A2A4A', '#1A5A8A', '#88CCEE', '#E0F0FF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[3], c[2], c[1], c[0]], 90);
Draw.hills(ctx, w, h, h * 0.08, 12, H.rgba(c[3], 0.3), 18);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(27);
for (var i = 0; i < 14; i++) {
var stx = rng() * w, sth = 20 + rng() * 60, stw = 4 + rng() * 8;
ctx.fillStyle = H.rgba(c[2], 0.4 + rng() * 0.3);
ctx.beginPath(); ctx.moveTo(stx - stw, 0); ctx.lineTo(stx, sth); ctx.lineTo(stx + stw, 0); ctx.closePath(); ctx.fill();
ctx.fillStyle = H.rgba(c[3], 0.2);
ctx.beginPath(); ctx.moveTo(stx - stw * 0.3, 0); ctx.lineTo(stx - 1, sth * 0.8); ctx.lineTo(stx + stw * 0.2, 0); ctx.closePath(); ctx.fill();
}
for (var j = 0; j < 10; j++) {
var sgx = rng() * w, sgh = 15 + rng() * 40, sgw = 5 + rng() * 10;
ctx.fillStyle = H.rgba(c[1], 0.5 + rng() * 0.3);
ctx.beginPath(); ctx.moveTo(sgx - sgw, h); ctx.lineTo(sgx, h - sgh); ctx.lineTo(sgx + sgw, h); ctx.closePath(); ctx.fill();
}
for (var k = 0; k < 8; k++) {
var kx = rng() * w, ky = h * 0.2 + rng() * h * 0.5 + Math.sin(t * 0.4 + k * 1.7) * 8, ks = 4 + rng() * 8;
Draw.polygon(ctx, kx, ky, ks, 6, H.rgba(c[3], 0.25 + Math.sin(t + k) * 0.1), t * 0.3 + k * 1.2);
Draw.circle(ctx, kx, ky, ks * 0.3, H.rgba('#FFF', 0.4));
}
ctx.save(); ctx.globalCompositeOperation = 'lighter';
for (var m = 0; m < 6; m++) Draw.circle(ctx, rng() * w + Math.sin(t * 0.5 + m) * 10, h * 0.3 + rng() * h * 0.3, 20 + rng() * 40, H.rgba(c[2], 0.02 + Math.sin(t * 0.8 + m * 1.5) * 0.015));
ctx.restore();
},
particles: { type: 'sparkles', count: 25, color: '#E0F0FF' }
},
treasure: {
meta: { id: 'underwater_treasure', mood: 'adventurous', colors: ['#0D2B45', '#1A4A6A', '#FFD700', '#FF8C00'], tags: ['treasure', 'gold', 'adventure', 'pirate'], description: 'A hidden underwater treasure trove glowing with golden riches', compatibleMusicMoods: ['adventurous', 'happy', 'upbeat'], recommendedFor: ['rpg', 'action', 'puzzle'] },
colors: ['#0D2B45', '#1A4A6A', '#FFD700', '#FF8C00'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[1], c[0]], 90);
sunRays(ctx, w, h, 3, '#FFDDAA', t);
Draw.hills(ctx, w, h, h * 0.75, 7, '#3A2A1A', 62);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(57), tcx = w * 0.5, tcy = h * 0.72;
ctx.fillStyle = '#6B3A1F'; ctx.fillRect(tcx - 25, tcy - 5, 50, 22);
ctx.fillStyle = '#8B5A2F'; ctx.beginPath(); ctx.ellipse(tcx, tcy - 5, 25, 10, 0, Math.PI, 0); ctx.fill();
ctx.fillStyle = '#7B4A25'; ctx.save(); ctx.translate(tcx - 25, tcy - 5); ctx.rotate(-0.6); ctx.fillRect(0, -16, 50, 16); ctx.restore();
var gp = 0.4 + Math.sin(t * 1.5) * 0.15;
Draw.glow(ctx, tcx, tcy - 10, 40, H.rgba(c[2], gp));
for (var g = 0; g < 12; g++) { var gx = tcx - 15 + rng() * 30, gy = tcy - 2 + rng() * 15; Draw.circle(ctx, gx, gy, 3 + rng() * 2, c[2]); Draw.circle(ctx, gx - 0.5, gy - 0.5, 2, H.lighten(c[2], 0.3)); }
ctx.strokeStyle = c[3]; ctx.lineWidth = 2; ctx.strokeRect(tcx - 24, tcy - 4, 48, 20);
for (var j = 0; j < 8; j++) {
var jx = tcx - 40 + rng() * 80, jy = tcy + 10 + rng() * 15, jC = ['#FF0044', '#00AAFF', '#44FF44', c[2]][Math.floor(rng() * 4)], jT = 0.5 + Math.sin(t * 3 + j * 2) * 0.3;
Draw.polygon(ctx, jx, jy, 2 + rng() * 3, 5 + Math.floor(rng() * 3), H.rgba(jC, jT), t * 0.2 + j);
Draw.glow(ctx, jx, jy, 6, H.rgba(jC, jT * 0.3));
}
for (var p = 0; p < 2; p++) {
var px = p === 0 ? w * 0.15 : w * 0.85;
ctx.fillStyle = '#4A4A3A'; ctx.fillRect(px - 8, h * 0.4, 16, h * 0.38); ctx.fillRect(px - 12, h * 0.38, 24, 6); ctx.fillRect(px - 12, h * 0.76, 24, 6);
ctx.strokeStyle = H.rgba('#2A2A20', 0.5); ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(px - 3, h * 0.5); ctx.lineTo(px + 2, h * 0.55); ctx.lineTo(px - 1, h * 0.62); ctx.stroke();
}
},
particles: { type: 'sparkles', count: 35, color: '#FFD700' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,466 @@
// urban.js — Urban theme for procedural background system
// 8 variants: citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('urban', {
citySkyline: {
meta: { id: 'urban_citySkyline', mood: 'inspiring', colors: ['#1A1A2E', '#2D2D44', '#4A4A66', '#FF8844'], tags: ['skyline', 'sunset', 'grand'], description: 'City skyline silhouetted against a glowing sunset', compatibleMusicMoods: ['epic', 'calm', 'ambient'], recommendedFor: ['story', 'rpg', 'creative'] },
colors: ['#1A1A2E', '#2D2D44', '#4A4A66', '#FF8844'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#1A0A2E', '#4A2255', '#CC6633', c[3]], 90);
// Sun glow on horizon
Draw.glow(ctx, w * 0.5, h * 0.58, 200, H.rgba(c[3], 0.3 + Math.sin(t * 0.2) * 0.05));
Draw.clouds(ctx, w, h, h * 0.12, 5, '#FF9966', 100);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Far buildings silhouette
Draw.buildings(ctx, w, h, h * 0.58, 20, [H.rgba(c[0], 0.7), H.rgba(c[1], 0.6)], 101);
// Main skyline
Draw.buildings(ctx, w, h, h * 0.65, 14, [c[0], c[1]], 102);
// Foreground buildings
Draw.buildings(ctx, w, h, h * 0.75, 8, [c[1], c[2]], 103);
// Ground
Draw.rect(ctx, 0, h * 0.88, w, h * 0.12, H.darken(c[0], 0.5));
// Antenna lights
var rng = H.seededRandom(104);
for (var i = 0; i < 6; i++) {
var ax = rng() * w;
var ay = h * 0.3 + rng() * h * 0.2;
var blink = Math.sin(t * 3 + i * 1.8) > 0.4 ? 0.9 : 0.15;
Draw.circle(ctx, ax, ay, 2, H.rgba('#FF3333', blink));
}
},
particles: { type: 'dust', count: 15, color: '#FFAA66' }
},
street: {
meta: { id: 'urban_street', mood: 'busy', colors: ['#2C3E50', '#566573', '#808B96', '#FFD700'], tags: ['road', 'traffic', 'evening'], description: 'Busy city street with lamp posts and passing lights', compatibleMusicMoods: ['energetic', 'ambient', 'electronic'], recommendedFor: ['racing', 'action', 'sports'] },
colors: ['#2C3E50', '#566573', '#808B96', '#FFD700'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#1A1A30', '#2A2A44', c[0]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Buildings on sides
Draw.buildings(ctx, w * 0.4, h, h * 0.15, 6, [c[0], c[1]], 201);
ctx.save(); ctx.translate(w * 0.6, 0);
Draw.buildings(ctx, w * 0.4, h, h * 0.15, 6, [c[0], c[1]], 202);
ctx.restore();
// Road surface (perspective)
ctx.fillStyle = '#333340';
ctx.beginPath();
ctx.moveTo(w * 0.3, h * 0.35);
ctx.lineTo(w * 0.7, h * 0.35);
ctx.lineTo(w, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fill();
// Road lines
var rng = H.seededRandom(203);
ctx.strokeStyle = H.rgba(c[3], 0.5);
ctx.lineWidth = 2;
ctx.setLineDash([15, 20]);
ctx.beginPath();
ctx.moveTo(w * 0.5, h * 0.35);
ctx.lineTo(w * 0.5, h);
ctx.stroke();
ctx.setLineDash([]);
// Lamp posts
for (var i = 0; i < 4; i++) {
var lx = w * 0.25 + (i % 2) * w * 0.5;
var ly = h * 0.4 + i * h * 0.15;
var lh = 40 + i * 10;
Draw.rect(ctx, lx - 2, ly - lh, 4, lh, '#555566');
Draw.rect(ctx, lx - 8, ly - lh - 3, 16, 5, '#666677');
// Lamp glow
var flicker = 0.6 + Math.sin(t * 4 + i * 2.5) * 0.05;
Draw.glow(ctx, lx, ly - lh, 35, H.rgba(c[3], 0.2 * flicker));
Draw.circle(ctx, lx, ly - lh, 3, H.rgba(c[3], flicker));
}
// Car headlights
for (var j = 0; j < 3; j++) {
var cy = (h * 0.5 + j * h * 0.15 + t * 20) % (h * 0.7) + h * 0.35;
var cx = w * 0.45 + rng() * w * 0.1;
var alpha = H.clamp((cy - h * 0.35) / (h * 0.4), 0, 0.7);
Draw.glow(ctx, cx - 5, cy, 6 * alpha + 3, H.rgba('#FFFFFF', alpha * 0.5));
Draw.glow(ctx, cx + 5, cy, 6 * alpha + 3, H.rgba('#FFFFFF', alpha * 0.5));
}
},
particles: { type: 'dust', count: 20, color: '#AAAAAA' }
},
rooftop: {
meta: { id: 'urban_rooftop', mood: 'reflective', colors: ['#1B2631', '#2C3E50', '#5D6D7E', '#F4D03F'], tags: ['night', 'view', 'quiet'], description: 'Rooftop view over a twinkling nighttime city', compatibleMusicMoods: ['calm', 'dreamy', 'ambient'], recommendedFor: ['story', 'puzzle', 'creative'] },
colors: ['#1B2631', '#2C3E50', '#5D6D7E', '#F4D03F'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#0A0A1A', '#151528', c[0]], 90);
Draw.stars(ctx, w, h * 0.4, 60, '#FFFFFF', 301, [0.3, 1.5]);
// Moon
Draw.glow(ctx, w * 0.8, h * 0.12, 50, H.rgba('#CCDDFF', 0.15));
Draw.circle(ctx, w * 0.8, h * 0.12, 18, '#DDDDEE');
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Distant city
Draw.buildings(ctx, w, h, h * 0.45, 25, [c[0], c[1]], 302);
// City lights twinkling
var rng = H.seededRandom(303);
for (var i = 0; i < 50; i++) {
var lx = rng() * w;
var ly = h * 0.3 + rng() * h * 0.25;
var twinkle = 0.2 + Math.sin(t * 2 + rng() * 10) * 0.3;
var lc = rng() > 0.7 ? c[3] : '#FFFFFF';
Draw.circle(ctx, lx, ly, 1, H.rgba(lc, twinkle));
}
// Rooftop surface
Draw.rect(ctx, 0, h * 0.7, w, h * 0.3, c[1]);
Draw.rect(ctx, 0, h * 0.7, w, 3, c[2]);
// Rooftop elements
Draw.rect(ctx, w * 0.05, h * 0.6, w * 0.08, h * 0.1, c[0]); // AC unit
Draw.rect(ctx, w * 0.85, h * 0.55, w * 0.04, h * 0.15, c[2]); // Antenna
// Railing
ctx.strokeStyle = c[2];
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, h * 0.68);
ctx.lineTo(w, h * 0.68);
ctx.stroke();
for (var j = 0; j < 20; j++) {
var rx = j * (w / 19);
ctx.beginPath();
ctx.moveTo(rx, h * 0.68);
ctx.lineTo(rx, h * 0.7);
ctx.stroke();
}
// String lights
ctx.strokeStyle = H.rgba('#332211', 0.5);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(w * 0.1, h * 0.62);
ctx.quadraticCurveTo(w * 0.5, h * 0.66, w * 0.9, h * 0.62);
ctx.stroke();
for (var k = 0; k < 12; k++) {
var bx = w * 0.1 + k * (w * 0.8 / 11);
var sag = Math.sin((k / 11) * Math.PI) * h * 0.04;
var by = h * 0.62 + sag;
var glow = 0.5 + Math.sin(t * 1.5 + k * 0.9) * 0.2;
Draw.glow(ctx, bx, by, 8, H.rgba(c[3], 0.2 * glow));
Draw.circle(ctx, bx, by, 2.5, H.rgba(c[3], glow));
}
},
particles: { type: 'fireflies', count: 8, color: '#FFD700' }
},
neonDistrict: {
meta: { id: 'urban_neonDistrict', mood: 'electric', colors: ['#0D0D1A', '#1A0A2E', '#FF00FF', '#00FFCC'], tags: ['neon', 'cyberpunk', 'vibrant'], description: 'Rain-slicked streets glowing with neon signs', compatibleMusicMoods: ['electronic', 'energetic', 'tense'], recommendedFor: ['action', 'racing', 'music'] },
colors: ['#0D0D1A', '#1A0A2E', '#FF00FF', '#00FFCC'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.darken(c[1], 0.3)], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Dark buildings
Draw.buildings(ctx, w, h, h * 0.1, 12, [c[0], H.darken(c[1], 0.3)], 401);
// Neon signs
var rng = H.seededRandom(402);
var neonColors = [c[2], c[3], '#FF4488', '#44AAFF', '#FFDD00'];
for (var i = 0; i < 8; i++) {
var nx = rng() * w;
var ny = h * 0.15 + rng() * h * 0.35;
var nw = 20 + rng() * 50;
var nh = 8 + rng() * 20;
var nc = neonColors[Math.floor(rng() * neonColors.length)];
var pulse = 0.6 + Math.sin(t * 2 + i * 1.7) * 0.25;
// Sign glow
Draw.glow(ctx, nx + nw / 2, ny + nh / 2, nw * 0.8, H.rgba(nc, 0.15 * pulse));
// Sign border
ctx.strokeStyle = H.rgba(nc, pulse);
ctx.lineWidth = 2;
ctx.strokeRect(nx, ny, nw, nh);
// Sign fill
ctx.fillStyle = H.rgba(nc, 0.08 * pulse);
ctx.fillRect(nx, ny, nw, nh);
}
// Wet ground
Draw.rect(ctx, 0, h * 0.75, w, h * 0.25, '#0A0A15');
// Neon reflections on ground
for (var j = 0; j < 6; j++) {
var rx = rng() * w;
var rc = neonColors[Math.floor(rng() * neonColors.length)];
var rAlpha = 0.05 + Math.sin(t * 1.5 + j * 2) * 0.03;
ctx.fillStyle = H.rgba(rc, rAlpha);
ctx.fillRect(rx - 25, h * 0.78, 50, h * 0.2);
}
// Power lines
ctx.strokeStyle = H.rgba('#333344', 0.5);
ctx.lineWidth = 1;
for (var k = 0; k < 3; k++) {
var py = h * 0.08 + k * 15;
ctx.beginPath();
ctx.moveTo(0, py);
ctx.quadraticCurveTo(w * 0.5, py + 10, w, py);
ctx.stroke();
}
// Scanline overlay
Draw.scanlines(ctx, w, h, 4, H.rgba(c[0], 0.06));
},
particles: { type: 'rain', count: 80, color: '#8899BB' }
},
subway: {
meta: { id: 'urban_subway', mood: 'gritty', colors: ['#1C1C1C', '#333333', '#555555', '#FFCC00'], tags: ['underground', 'industrial', 'dark'], description: 'Underground subway platform with arriving train lights', compatibleMusicMoods: ['tense', 'electronic', 'ambient'], recommendedFor: ['action', 'puzzle', 'rpg'] },
colors: ['#1C1C1C', '#333333', '#555555', '#FFCC00'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[0]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Ceiling
Draw.rect(ctx, 0, 0, w, h * 0.15, c[0]);
// Ceiling panels
var rng = H.seededRandom(501);
for (var i = 0; i < 10; i++) {
var px = i * (w / 10) + 5;
Draw.rect(ctx, px, h * 0.02, w / 10 - 10, h * 0.1, H.lighten(c[0], 0.05));
}
// Fluorescent lights
for (var j = 0; j < 6; j++) {
var lx = w * 0.1 + j * (w * 0.8 / 5);
var flicker = 0.7 + Math.sin(t * 8 + j * 3) * 0.1 + (rng() > 0.97 ? -0.4 : 0);
Draw.rect(ctx, lx - 20, h * 0.12, 40, 4, H.rgba('#FFFFFF', flicker));
Draw.glow(ctx, lx, h * 0.2, 50, H.rgba('#FFFFFF', 0.08 * flicker));
}
// Walls (tiled)
Draw.rect(ctx, 0, h * 0.15, w, h * 0.45, c[1]);
Draw.grid(ctx, w, h * 0.45, 25, H.rgba(c[2], 0.15), 0.5);
// Colored tile stripe
Draw.rect(ctx, 0, h * 0.35, w, 8, c[3]);
// Platform
Draw.rect(ctx, 0, h * 0.6, w, h * 0.08, c[2]);
// Yellow safety line
Draw.rect(ctx, 0, h * 0.6, w, 4, H.rgba(c[3], 0.8));
// Track area
Draw.rect(ctx, 0, h * 0.68, w, h * 0.32, H.darken(c[0], 0.3));
// Rails
ctx.strokeStyle = H.rgba('#888888', 0.4);
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(0, h * 0.78); ctx.lineTo(w, h * 0.78); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, h * 0.88); ctx.lineTo(w, h * 0.88); ctx.stroke();
// Train approaching light
var approach = (Math.sin(t * 0.3) + 1) * 0.5;
if (approach > 0.6) {
var intensity = (approach - 0.6) / 0.4;
Draw.glow(ctx, w * 0.5, h * 0.82, 80 * intensity, H.rgba('#FFFFFF', 0.3 * intensity));
Draw.glow(ctx, w * 0.4, h * 0.82, 20 * intensity, H.rgba('#FFFFFF', 0.5 * intensity));
Draw.glow(ctx, w * 0.6, h * 0.82, 20 * intensity, H.rgba('#FFFFFF', 0.5 * intensity));
}
// Bench
Draw.rect(ctx, w * 0.15, h * 0.54, w * 0.12, h * 0.04, '#444444');
Draw.rect(ctx, w * 0.16, h * 0.56, 4, h * 0.04, '#444444');
Draw.rect(ctx, w * 0.26, h * 0.56, 4, h * 0.04, '#444444');
},
particles: { type: 'dust', count: 15, color: '#888888' }
},
park: {
meta: { id: 'urban_park', mood: 'relaxed', colors: ['#2E7D32', '#4CAF50', '#81C784', '#795548'], tags: ['trees', 'nature', 'city'], description: 'A peaceful city park with trees and walking paths', compatibleMusicMoods: ['calm', 'happy', 'dreamy'], recommendedFor: ['pet', 'puzzle', 'story', 'creative'] },
colors: ['#2E7D32', '#4CAF50', '#81C784', '#795548'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, ['#66AADD', '#88CCEE', '#BBDDEE'], 90);
Draw.clouds(ctx, w, h, h * 0.08, 5, '#FFFFFF', 600);
// Distant buildings
Draw.buildings(ctx, w, h, h * 0.35, 12, ['#8899AA', '#99AABB'], 601);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Far tree line
Draw.hills(ctx, w, h, h * 0.45, 8, H.darken(c[0], 0.2), 602);
Draw.trees(ctx, w, h, h * 0.5, 15, c[3], c[0], 603);
// Park ground
Draw.hills(ctx, w, h, h * 0.6, 5, c[1], 604);
Draw.rect(ctx, 0, h * 0.68, w, h * 0.32, c[2]);
// Walking path
ctx.strokeStyle = H.rgba('#CCBBAA', 0.6);
ctx.lineWidth = 12;
ctx.beginPath();
ctx.moveTo(0, h * 0.82);
ctx.quadraticCurveTo(w * 0.3, h * 0.75, w * 0.5, h * 0.78);
ctx.quadraticCurveTo(w * 0.7, h * 0.82, w, h * 0.76);
ctx.stroke();
// Park trees
Draw.trees(ctx, w, h, h * 0.72, 8, c[3], c[1], 605);
// Park bench
Draw.rect(ctx, w * 0.6, h * 0.7, 25, 3, c[3]);
Draw.rect(ctx, w * 0.602, h * 0.66, 3, h * 0.04, c[3]);
Draw.rect(ctx, w * 0.63, h * 0.66, 3, h * 0.04, c[3]);
// Pond
ctx.fillStyle = H.rgba('#4488AA', 0.4);
ctx.beginPath();
ctx.ellipse(w * 0.25, h * 0.85, 50, 20, 0, 0, Math.PI * 2);
ctx.fill();
// Pond shimmer
var shimmer = 0.15 + Math.sin(t * 1.5) * 0.05;
ctx.fillStyle = H.rgba('#AADDEE', shimmer);
ctx.beginPath();
ctx.ellipse(w * 0.24, h * 0.84, 15, 5, 0.2, 0, Math.PI * 2);
ctx.fill();
// Lamp post
Draw.rect(ctx, w * 0.45 - 2, h * 0.58, 4, h * 0.14, '#555555');
Draw.circle(ctx, w * 0.45, h * 0.58, 5, H.rgba('#FFEE88', 0.3 + Math.sin(t) * 0.05));
},
particles: { type: 'leaves', count: 12, color: '#CC9944' }
},
mall: {
meta: { id: 'urban_mall', mood: 'lively', colors: ['#E8E8E8', '#CCCCCC', '#888888', '#FF6B6B'], tags: ['indoor', 'bright', 'commercial'], description: 'Bright mall interior with shop windows and escalators', compatibleMusicMoods: ['happy', 'energetic', 'electronic'], recommendedFor: ['trivia', 'creative', 'pet'] },
colors: ['#E8E8E8', '#CCCCCC', '#888888', '#FF6B6B'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[0]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(701);
// Ceiling structure
Draw.rect(ctx, 0, 0, w, h * 0.08, c[2]);
// Skylight panels
for (var i = 0; i < 4; i++) {
var sx = w * 0.15 + i * (w * 0.7 / 3);
Draw.rect(ctx, sx, h * 0.01, w * 0.12, h * 0.05, H.rgba('#AADDFF', 0.3));
}
// Ceiling lights
for (var j = 0; j < 8; j++) {
var lx = w * 0.1 + j * (w * 0.8 / 7);
Draw.rect(ctx, lx - 12, h * 0.07, 24, 3, '#FFFFFF');
Draw.glow(ctx, lx, h * 0.12, 30, H.rgba('#FFFFFF', 0.06));
}
// Floor
Draw.rect(ctx, 0, h * 0.7, w, h * 0.3, '#DDDDDD');
// Floor tiles
Draw.grid(ctx, w, h * 0.3, 40, H.rgba('#BBBBBB', 0.3), 0.5);
// Store fronts - left side
var shopColors = ['#FF6B6B', '#4FC3F7', '#81C784', '#FFB74D', '#BA68C8'];
for (var k = 0; k < 3; k++) {
var sy = h * 0.12 + k * h * 0.2;
var sc = shopColors[Math.floor(rng() * shopColors.length)];
// Shop frame
Draw.rect(ctx, 0, sy, w * 0.2, h * 0.18, '#F5F5F5');
Draw.rect(ctx, w * 0.01, sy + h * 0.01, w * 0.18, h * 0.12, H.rgba(sc, 0.15));
// Shop sign
var signPulse = 0.7 + Math.sin(t * 1.5 + k * 2.3) * 0.15;
Draw.rect(ctx, w * 0.03, sy + h * 0.005, w * 0.14, h * 0.025, H.rgba(sc, signPulse));
}
// Store fronts - right side
for (var m = 0; m < 3; m++) {
var rsy = h * 0.12 + m * h * 0.2;
var rsc = shopColors[Math.floor(rng() * shopColors.length)];
Draw.rect(ctx, w * 0.8, rsy, w * 0.2, h * 0.18, '#F5F5F5');
Draw.rect(ctx, w * 0.81, rsy + h * 0.01, w * 0.18, h * 0.12, H.rgba(rsc, 0.15));
var rPulse = 0.7 + Math.sin(t * 1.5 + m * 2 + 1) * 0.15;
Draw.rect(ctx, w * 0.83, rsy + h * 0.005, w * 0.14, h * 0.025, H.rgba(rsc, rPulse));
}
// Escalator
Draw.rect(ctx, w * 0.4, h * 0.4, w * 0.2, h * 0.35, c[2]);
// Escalator steps (animated)
for (var s = 0; s < 8; s++) {
var stepY = h * 0.42 + ((s * h * 0.04 + t * 15) % (h * 0.3));
ctx.strokeStyle = H.rgba('#AAAAAA', 0.5);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(w * 0.42, stepY);
ctx.lineTo(w * 0.58, stepY);
ctx.stroke();
}
// Railing
ctx.strokeStyle = c[2];
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(w * 0.4, h * 0.4); ctx.lineTo(w * 0.4, h * 0.75); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w * 0.6, h * 0.4); ctx.lineTo(w * 0.6, h * 0.75); ctx.stroke();
// Potted plant
Draw.rect(ctx, w * 0.32, h * 0.64, 14, 12, '#8B6914');
Draw.circle(ctx, w * 0.325, h * 0.6, 12, '#4CAF50');
},
particles: { type: 'sparkles', count: 20, color: '#FFFFFF' }
},
alley: {
meta: { id: 'urban_alley', mood: 'mysterious', colors: ['#111118', '#1E1E28', '#333340', '#BB8844'], tags: ['dark', 'narrow', 'moody'], description: 'A narrow alley with a single warm light overhead', compatibleMusicMoods: ['tense', 'ambient', 'electronic'], recommendedFor: ['rpg', 'action', 'story'] },
colors: ['#111118', '#1E1E28', '#333340', '#BB8844'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[0]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(801);
// Left wall
Draw.rect(ctx, 0, 0, w * 0.3, h, c[1]);
// Right wall
Draw.rect(ctx, w * 0.7, 0, w * 0.3, h, c[1]);
// Brick texture (left)
for (var row = 0; row < 30; row++) {
var by = row * (h / 30);
var offset = (row % 2) * 8;
for (var col = 0; col < 4; col++) {
var bx = col * (w * 0.3 / 4) + offset;
ctx.strokeStyle = H.rgba(c[2], 0.15);
ctx.lineWidth = 0.5;
ctx.strokeRect(bx, by, w * 0.3 / 4 - 2, h / 30 - 2);
}
}
// Brick texture (right)
for (var row2 = 0; row2 < 30; row2++) {
var by2 = row2 * (h / 30);
var offset2 = (row2 % 2) * 8;
for (var col2 = 0; col2 < 4; col2++) {
var bx2 = w * 0.7 + col2 * (w * 0.3 / 4) + offset2;
ctx.strokeStyle = H.rgba(c[2], 0.15);
ctx.lineWidth = 0.5;
ctx.strokeRect(bx2, by2, w * 0.3 / 4 - 2, h / 30 - 2);
}
}
// Ground
Draw.rect(ctx, w * 0.28, h * 0.85, w * 0.44, h * 0.15, H.darken(c[1], 0.3));
// Puddle
ctx.fillStyle = H.rgba('#223344', 0.4);
ctx.beginPath();
ctx.ellipse(w * 0.5, h * 0.92, 35, 8, 0, 0, Math.PI * 2);
ctx.fill();
// Puddle light reflection
var refPulse = 0.15 + Math.sin(t * 1.2) * 0.05;
ctx.fillStyle = H.rgba(c[3], refPulse);
ctx.beginPath();
ctx.ellipse(w * 0.5, h * 0.92, 15, 4, 0, 0, Math.PI * 2);
ctx.fill();
// Overhead light
Draw.rect(ctx, w * 0.48, h * 0.15, w * 0.04, h * 0.02, '#555555');
var lightFlicker = 0.7 + Math.sin(t * 6) * 0.15 + Math.sin(t * 13) * 0.05;
Draw.glow(ctx, w * 0.5, h * 0.18, 100, H.rgba(c[3], 0.18 * lightFlicker));
Draw.glow(ctx, w * 0.5, h * 0.18, 40, H.rgba(c[3], 0.3 * lightFlicker));
Draw.circle(ctx, w * 0.5, h * 0.17, 4, H.rgba('#FFDDAA', lightFlicker));
// Light cone
ctx.fillStyle = H.rgba(c[3], 0.04 * lightFlicker);
ctx.beginPath();
ctx.moveTo(w * 0.48, h * 0.18);
ctx.lineTo(w * 0.3, h * 0.85);
ctx.lineTo(w * 0.7, h * 0.85);
ctx.closePath();
ctx.fill();
// Dumpster
Draw.rect(ctx, w * 0.62, h * 0.7, w * 0.07, h * 0.12, '#2A3A2A');
Draw.rect(ctx, w * 0.615, h * 0.69, w * 0.08, h * 0.02, '#334433');
// Fire escape (left wall)
ctx.strokeStyle = H.rgba(c[2], 0.4);
ctx.lineWidth = 2;
for (var fe = 0; fe < 4; fe++) {
var fey = h * 0.2 + fe * h * 0.15;
ctx.beginPath();
ctx.moveTo(w * 0.3, fey);
ctx.lineTo(w * 0.38, fey);
ctx.lineTo(w * 0.38, fey + h * 0.03);
ctx.stroke();
}
// Steam wisps
var steamAlpha = 0.05 + Math.sin(t * 0.8) * 0.03;
Draw.glow(ctx, w * 0.65, h * 0.68, 25, H.rgba('#AAAAAA', steamAlpha));
},
particles: { type: 'fog', count: 6, color: '#555555' }
}
});
})(window.BackgroundEngine);
@@ -0,0 +1,476 @@
// weather.js — Weather and atmospheric background themes
// Variants: rain, snow, storm, sunset, sunrise, fog, lightning, aurora
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
engine.registerTheme('weather', {
rain: {
meta: { id: 'weather_rain', mood: 'melancholy', colors: ['#3a4a5a', '#2a3a4a', '#5a6a7a', '#8899aa'], tags: ['rain', 'wet', 'gray', 'calm'], description: 'Overcast sky with steady rainfall, puddles, and muted city outlines', compatibleMusicMoods: ['melancholy', 'calm', 'ambient'], recommendedFor: ['story', 'puzzle', 'rpg'] },
colors: ['#3a4a5a', '#2a3a4a', '#5a6a7a', '#8899aa'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Low heavy clouds
var rng = H.seededRandom(111);
for (var i = 0; i < 8; i++) {
var cx = rng() * w * 1.2 - w * 0.1;
var cy = h * 0.05 + rng() * h * 0.2;
var cw = 100 + rng() * 200;
var drift = Math.sin(t * 0.1 + i) * 5;
ctx.fillStyle = H.rgba(c[2], 0.2 + rng() * 0.15);
ctx.beginPath(); ctx.ellipse(cx + drift, cy, cw, 30 + rng() * 20, 0, 0, Math.PI * 2); ctx.fill();
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(112);
// Distant buildings silhouette
Draw.buildings(ctx, w, h, h * 0.75, 12, [H.darken(c[1], 0.2), H.darken(c[0], 0.25)], 113);
// Ground with puddles
Draw.rect(ctx, 0, h * 0.75, w, h * 0.25, H.darken(c[1], 0.35));
for (var p = 0; p < 6; p++) {
var px = rng() * w;
var pw = 30 + rng() * 60;
var ripple = Math.sin(t * 3 + p * 2) * 0.08;
ctx.fillStyle = H.rgba(c[3], 0.1 + ripple);
ctx.beginPath(); ctx.ellipse(px, h * 0.82 + rng() * h * 0.1, pw, 6, 0, 0, Math.PI * 2); ctx.fill();
// Ripple rings
var ringR = 5 + (t * 20 + p * 30) % 20;
ctx.strokeStyle = H.rgba('#FFFFFF', 0.08 * (1 - ringR / 25));
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.arc(px, h * 0.82 + rng() * h * 0.1, ringR, 0, Math.PI * 2); ctx.stroke();
}
// Mist layer near ground
for (var m = 0; m < 5; m++) {
ctx.fillStyle = H.rgba(c[3], 0.04);
ctx.beginPath();
ctx.ellipse(rng() * w, h * 0.73 + rng() * h * 0.05, 120 + rng() * 100, 15, 0, 0, Math.PI * 2);
ctx.fill();
}
},
particles: { type: 'rain', count: 150, color: '#99aabb' }
},
snow: {
meta: { id: 'weather_snow', mood: 'peaceful', colors: ['#c8d8e8', '#a0b8cc', '#e8eef4', '#FFFFFF'], tags: ['snow', 'winter', 'cold', 'peaceful'], description: 'Gentle snowfall over a soft winter landscape with frosted trees', compatibleMusicMoods: ['peaceful', 'calm', 'dreamy'], recommendedFor: ['puzzle', 'pet', 'creative', 'story'] },
colors: ['#c8d8e8', '#a0b8cc', '#e8eef4', '#FFFFFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[1], c[0]], 90);
// Snow-covered ground
Draw.hills(ctx, w, h, h * 0.7, 6, c[2], 221);
Draw.rect(ctx, 0, h * 0.75, w, h * 0.25, c[2]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(222);
// Frosted trees
for (var i = 0; i < 10; i++) {
var tx = rng() * w;
var ty = h * 0.55 + rng() * h * 0.18;
var th = 40 + rng() * 60;
var tw = th * 0.4;
// Trunk
Draw.rect(ctx, tx - 3, ty, 6, th * 0.3, '#665544');
// Snow-covered canopy layers
for (var l = 0; l < 3; l++) {
var ly = ty - th * (0.1 + l * 0.25);
var lw = tw * (1 - l * 0.25);
ctx.fillStyle = H.blendColor('#4a7a4a', c[2], 0.4 + l * 0.2);
ctx.beginPath();
ctx.moveTo(tx - lw, ly + 15);
ctx.lineTo(tx, ly);
ctx.lineTo(tx + lw, ly + 15);
ctx.closePath(); ctx.fill();
// Snow caps
ctx.fillStyle = H.rgba(c[3], 0.7);
ctx.beginPath();
ctx.moveTo(tx - lw * 0.6, ly + 8);
ctx.lineTo(tx, ly - 2);
ctx.lineTo(tx + lw * 0.6, ly + 8);
ctx.closePath(); ctx.fill();
}
}
// Snow drifts
for (var d = 0; d < 5; d++) {
var dx = rng() * w;
ctx.fillStyle = H.rgba(c[3], 0.3);
ctx.beginPath();
ctx.ellipse(dx, h * 0.73 + rng() * h * 0.05, 40 + rng() * 60, 8, 0, 0, Math.PI * 2);
ctx.fill();
}
// Distant cabin
var cabX = w * 0.7;
Draw.rect(ctx, cabX, h * 0.56, 50, 35, '#5a3a2a');
ctx.fillStyle = '#6a4a3a';
ctx.beginPath(); ctx.moveTo(cabX - 5, h * 0.56); ctx.lineTo(cabX + 25, h * 0.48); ctx.lineTo(cabX + 55, h * 0.56); ctx.closePath(); ctx.fill();
// Snow on roof
ctx.fillStyle = c[2];
ctx.beginPath(); ctx.moveTo(cabX - 5, h * 0.56); ctx.lineTo(cabX + 25, h * 0.475); ctx.lineTo(cabX + 55, h * 0.56); ctx.closePath(); ctx.fill();
// Chimney smoke
var smokeAlpha = 0.06 + Math.sin(t * 0.5) * 0.02;
for (var s = 0; s < 4; s++) {
var sx = cabX + 40 + Math.sin(t * 0.4 + s) * 6;
var sy = h * 0.42 - s * 15;
ctx.fillStyle = H.rgba('#CCCCCC', smokeAlpha * (1 - s * 0.2));
ctx.beginPath(); ctx.ellipse(sx, sy, 8 + s * 5, 5 + s * 3, 0, 0, Math.PI * 2); ctx.fill();
}
// Window glow
Draw.rect(ctx, cabX + 15, h * 0.57, 10, 12, H.rgba('#ffcc44', 0.6));
Draw.glow(ctx, cabX + 20, h * 0.63, 20, '#ffcc44');
},
particles: { type: 'snow', count: 100, color: '#FFFFFF' }
},
storm: {
meta: { id: 'weather_storm', mood: 'intense', colors: ['#1a1a25', '#252535', '#3a3a55', '#667788'], tags: ['storm', 'dark', 'wind', 'dramatic'], description: 'Dark storm clouds with heavy rain, wind-bent trees, and rolling thunder', compatibleMusicMoods: ['intense', 'dramatic', 'tense'], recommendedFor: ['action', 'rpg', 'racing'] },
colors: ['#1a1a25', '#252535', '#3a3a55', '#667788'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Turbulent clouds
var rng = H.seededRandom(331);
for (var i = 0; i < 12; i++) {
var cx = rng() * w * 1.3 - w * 0.15;
var cy = rng() * h * 0.35;
var cw = 80 + rng() * 200;
var ch = 25 + rng() * 40;
var drift = Math.sin(t * 0.3 + i * 0.7) * 10;
ctx.fillStyle = H.rgba(c[2], 0.2 + rng() * 0.2);
ctx.beginPath(); ctx.ellipse(cx + drift, cy, cw, ch, 0, 0, Math.PI * 2); ctx.fill();
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(332);
// Dark ground
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, H.darken(c[0], 0.4));
Draw.hills(ctx, w, h, h * 0.78, 5, H.darken(c[1], 0.3), 333);
// Wind-bent trees
for (var i = 0; i < 6; i++) {
var tx = rng() * w;
var sway = Math.sin(t * 2 + i) * 8;
ctx.strokeStyle = '#3a2a1a'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(tx, h * 0.78); ctx.quadraticCurveTo(tx + sway * 0.5, h * 0.65, tx + sway, h * 0.58); ctx.stroke();
// Branches blown sideways
for (var b = 0; b < 3; b++) {
var by = h * 0.62 + b * 0.05 * h;
var bSway = sway * (1.2 - b * 0.2);
ctx.strokeStyle = '#3a2a1a'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(tx + sway * (0.5 + b * 0.1), by);
ctx.lineTo(tx + bSway + 15 + rng() * 10, by - 5 - rng() * 8); ctx.stroke();
}
}
// Distant lightning flash (periodic)
var flashCycle = (t * 0.7) % 6;
if (flashCycle < 0.1) {
ctx.fillStyle = H.rgba('#FFFFFF', 0.08);
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = H.rgba('#ccddff', 0.15);
ctx.fillRect(0, 0, w, h * 0.4);
}
// Power lines
ctx.strokeStyle = H.rgba('#444', 0.5); ctx.lineWidth = 1;
for (var pl = 0; pl < 3; pl++) {
ctx.beginPath(); ctx.moveTo(0, h * 0.5 + pl * 10);
for (var px = 0; px < w; px += 5) {
ctx.lineTo(px, h * 0.5 + pl * 10 + Math.sin(px * 0.008 + t * 0.5) * 4);
}
ctx.stroke();
}
// Utility pole
Draw.rect(ctx, w * 0.3, h * 0.35, 4, h * 0.43, '#4a3a2a');
Draw.rect(ctx, w * 0.27, h * 0.38, 30, 3, '#4a3a2a');
Draw.rect(ctx, w * 0.7, h * 0.35, 4, h * 0.43, '#4a3a2a');
Draw.rect(ctx, w * 0.67, h * 0.38, 30, 3, '#4a3a2a');
},
particles: { type: 'rain', count: 250, color: '#7788aa' }
},
sunset: {
meta: { id: 'weather_sunset', mood: 'warm', colors: ['#1a0a2e', '#cc4400', '#ff8833', '#ffd166'], tags: ['sunset', 'golden', 'warm', 'beautiful'], description: 'Vibrant sunset with orange-purple gradient, silhouette landscape, and warm glow', compatibleMusicMoods: ['warm', 'romantic', 'calm'], recommendedFor: ['story', 'creative', 'pet', 'music'] },
colors: ['#1a0a2e', '#cc4400', '#ff8833', '#ffd166'],
renderBase: function(ctx, w, h, c, Draw, t) {
var pulse = Math.sin(t * 0.15) * 0.05;
ctx.fillStyle = Draw.linearGradient(ctx, 0, 0, 0, h, [
[0, c[0]], [0.3, H.blendColor(c[0], c[1], 0.5 + pulse)],
[0.5, c[1]], [0.65, c[2]], [0.8, c[3]], [1, H.lighten(c[3], 0.3)]
]);
ctx.fillRect(0, 0, w, h);
// Sun
var sunY = h * 0.52 + Math.sin(t * 0.05) * 3;
Draw.glow(ctx, w * 0.5, sunY, 120, c[3]);
Draw.glow(ctx, w * 0.5, sunY, 60, '#FFFFFF');
Draw.circle(ctx, w * 0.5, sunY, 30, c[3]);
Draw.circle(ctx, w * 0.5, sunY, 22, H.lighten(c[3], 0.4));
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Silhouette mountains
Draw.mountains(ctx, w, h, h * 0.65, 8, '#1a0a15', 441);
Draw.mountains(ctx, w, h, h * 0.72, 6, '#0d050a', 442);
// Silhouette trees on ridge
var rng = H.seededRandom(443);
for (var i = 0; i < 15; i++) {
var tx = rng() * w;
var ty = h * 0.65 + rng() * h * 0.08;
var th = 15 + rng() * 30;
ctx.fillStyle = '#0d050a';
ctx.beginPath(); ctx.moveTo(tx - th * 0.3, ty); ctx.lineTo(tx, ty - th); ctx.lineTo(tx + th * 0.3, ty); ctx.closePath(); ctx.fill();
}
// Clouds catching light
Draw.clouds(ctx, w, h, h * 0.15, 5, '#ff6633', 444);
// Water reflection
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, H.rgba(c[1], 0.3));
for (var r = 0; r < 8; r++) {
var ry = h * 0.8 + r * h * 0.025;
ctx.strokeStyle = H.rgba(c[3], 0.06 - r * 0.005);
ctx.lineWidth = 2;
ctx.beginPath();
for (var x = 0; x < w; x += 4) {
ctx.lineTo(x, ry + Math.sin(x * 0.015 + t * 0.5 + r) * 2);
}
ctx.stroke();
}
// Birds silhouettes
for (var b = 0; b < 5; b++) {
var bx = w * 0.2 + rng() * w * 0.6 + Math.sin(t * 0.3 + b) * 10;
var by = h * 0.15 + rng() * h * 0.15;
ctx.strokeStyle = '#1a0a15'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(bx - 4, by, 5, -0.8, 0.1); ctx.stroke();
ctx.beginPath(); ctx.arc(bx + 4, by, 5, Math.PI - 0.1, Math.PI + 0.8); ctx.stroke();
}
},
particles: { type: 'sparkles', count: 15, color: '#FFD166' }
},
sunrise: {
meta: { id: 'weather_sunrise', mood: 'hopeful', colors: ['#2a1a40', '#884466', '#ee8855', '#ffe4a0'], tags: ['sunrise', 'morning', 'fresh', 'bright'], description: 'Early morning sunrise with pink-orange sky, mist, and dewy fields', compatibleMusicMoods: ['hopeful', 'upbeat', 'calm'], recommendedFor: ['puzzle', 'pet', 'creative', 'sports'] },
colors: ['#2a1a40', '#884466', '#ee8855', '#ffe4a0'],
renderBase: function(ctx, w, h, c, Draw, t) {
var shift = Math.sin(t * 0.1) * 0.03;
ctx.fillStyle = Draw.linearGradient(ctx, 0, 0, 0, h, [
[0, c[0]], [0.25, c[1]], [0.5, c[2]],
[0.7, H.lighten(c[3], shift)], [1, H.lighten(c[3], 0.3)]
]);
ctx.fillRect(0, 0, w, h);
// Rising sun at horizon
var sunY = h * 0.58 - Math.sin(t * 0.02) * 2;
Draw.glow(ctx, w * 0.35, sunY, 150, c[3]);
Draw.glow(ctx, w * 0.35, sunY, 70, '#FFFFFF');
Draw.circle(ctx, w * 0.35, sunY, 35, '#fff8e0');
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Rolling hills
Draw.hills(ctx, w, h, h * 0.65, 5, '#3a6a3a', 551);
Draw.hills(ctx, w, h, h * 0.72, 7, '#2a5a2a', 552);
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, '#2a4a2a');
// Morning mist layers
for (var m = 0; m < 6; m++) {
var my = h * 0.6 + m * h * 0.04;
var drift = Math.sin(t * 0.15 + m * 0.8) * 15;
ctx.fillStyle = H.rgba('#ffe8cc', 0.04 - m * 0.004);
ctx.beginPath();
ctx.ellipse(w * 0.3 + drift + m * 40, my, 200 + m * 30, 15, 0, 0, Math.PI * 2);
ctx.fill();
}
// Dew sparkles on grass
var rng = H.seededRandom(553);
for (var d = 0; d < 20; d++) {
var dx = rng() * w;
var dy = h * 0.73 + rng() * h * 0.18;
var sparkle = Math.sin(t * 3 + d * 1.5) > 0.7 ? 0.6 : 0.1;
Draw.circle(ctx, dx, dy, 1.5, H.rgba('#FFFFFF', sparkle));
}
// Fence posts
for (var f = 0; f < 8; f++) {
var fx = w * 0.1 + f * w * 0.11;
Draw.rect(ctx, fx, h * 0.68, 3, h * 0.08, '#5a4a3a');
}
ctx.strokeStyle = '#5a4a3a'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(w * 0.1, h * 0.71); ctx.lineTo(w * 0.87, h * 0.71); ctx.stroke();
ctx.beginPath(); ctx.moveTo(w * 0.1, h * 0.74); ctx.lineTo(w * 0.87, h * 0.74); ctx.stroke();
// Light rays from sun
for (var r = 0; r < 6; r++) {
var angle = -0.4 + r * 0.16;
var rayAlpha = 0.02 + Math.sin(t * 0.3 + r) * 0.01;
ctx.fillStyle = H.rgba(c[3], rayAlpha);
ctx.beginPath();
ctx.moveTo(w * 0.35, h * 0.58);
ctx.lineTo(w * 0.35 + Math.cos(angle) * w, h * 0.58 + Math.sin(angle) * h);
ctx.lineTo(w * 0.35 + Math.cos(angle + 0.05) * w, h * 0.58 + Math.sin(angle + 0.05) * h);
ctx.closePath(); ctx.fill();
}
},
particles: { type: 'dust', count: 15, color: '#FFE8AA' }
},
fog: {
meta: { id: 'weather_fog', mood: 'eerie', colors: ['#8a8a8a', '#666666', '#aaaaaa', '#cccccc'], tags: ['fog', 'mist', 'mysterious', 'gray'], description: 'Thick fog with barely visible shapes, faded landmarks, and diffused light', compatibleMusicMoods: ['eerie', 'mysterious', 'ambient'], recommendedFor: ['story', 'rpg', 'puzzle'] },
colors: ['#8a8a8a', '#666666', '#aaaaaa', '#cccccc'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[1], c[0], c[2]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(661);
// Ghost buildings barely visible
for (var b = 0; b < 5; b++) {
var bx = rng() * w;
var bw = 30 + rng() * 50;
var bh = 60 + rng() * 100;
ctx.fillStyle = H.rgba(c[1], 0.08);
ctx.fillRect(bx, h * 0.5 - bh, bw, bh + h * 0.5);
}
// Ground fade
Draw.rect(ctx, 0, h * 0.7, w, h * 0.3, H.rgba(c[1], 0.2));
// Faded streetlamps
for (var l = 0; l < 3; l++) {
var lx = w * 0.2 + l * w * 0.3;
Draw.rect(ctx, lx, h * 0.35, 3, h * 0.35, H.rgba('#555', 0.3));
var lampGlow = 0.15 + Math.sin(t * 0.4 + l) * 0.05;
Draw.circle(ctx, lx + 1, h * 0.34, 6, H.rgba('#ffdd88', lampGlow));
Draw.glow(ctx, lx + 1, h * 0.34, 60, H.rgba('#ffdd88', lampGlow * 0.6));
}
// Rolling fog layers
for (var f = 0; f < 10; f++) {
var fx = (rng() * w * 1.5 - w * 0.25) + Math.sin(t * 0.08 + f * 0.6) * 30;
var fy = h * 0.2 + rng() * h * 0.6;
var fs = 100 + rng() * 200;
ctx.fillStyle = H.rgba(c[2], 0.06 + rng() * 0.04);
ctx.beginPath(); ctx.ellipse(fx, fy, fs, fs * 0.3, 0, 0, Math.PI * 2); ctx.fill();
}
// Road disappearing into fog
ctx.fillStyle = H.rgba('#555', 0.1);
ctx.beginPath();
ctx.moveTo(w * 0.35, h); ctx.lineTo(w * 0.48, h * 0.4);
ctx.lineTo(w * 0.52, h * 0.4); ctx.lineTo(w * 0.65, h);
ctx.closePath(); ctx.fill();
// Dashed center line
ctx.strokeStyle = H.rgba('#ddcc44', 0.06);
ctx.lineWidth = 2; ctx.setLineDash([10, 15]);
ctx.beginPath(); ctx.moveTo(w * 0.5, h); ctx.lineTo(w * 0.5, h * 0.4); ctx.stroke();
ctx.setLineDash([]);
},
particles: { type: 'fog', count: 18, color: '#BBBBBB' }
},
lightning: {
meta: { id: 'weather_lightning', mood: 'dramatic', colors: ['#0a0a18', '#1a1a30', '#3344aa', '#eeeeff'], tags: ['lightning', 'thunder', 'electric', 'dark'], description: 'Pitch-dark sky lit by dramatic lightning bolts and electric-blue highlights', compatibleMusicMoods: ['dramatic', 'intense', 'electronic'], recommendedFor: ['action', 'rpg', 'racing'] },
colors: ['#0a0a18', '#1a1a30', '#3344aa', '#eeeeff'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Cloud mass
var rng = H.seededRandom(771);
for (var i = 0; i < 10; i++) {
var cx = rng() * w;
var cy = rng() * h * 0.3;
ctx.fillStyle = H.rgba(c[2], 0.08 + rng() * 0.06);
ctx.beginPath(); ctx.ellipse(cx, cy, 80 + rng() * 150, 25 + rng() * 30, 0, 0, Math.PI * 2); ctx.fill();
}
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(772);
// Dark landscape
Draw.mountains(ctx, w, h, h * 0.75, 6, H.darken(c[0], 0.3), 773);
Draw.rect(ctx, 0, h * 0.82, w, h * 0.18, H.darken(c[0], 0.4));
// Lightning bolt (procedural, periodic)
var boltPhase = (t * 0.6) % 5;
if (boltPhase < 0.15 || (boltPhase > 2 && boltPhase < 2.08)) {
var boltRng = H.seededRandom(Math.floor(t * 0.6) * 100);
var bx = w * 0.2 + boltRng() * w * 0.6;
var segments = 8 + Math.floor(boltRng() * 5);
ctx.strokeStyle = c[3]; ctx.lineWidth = 3;
ctx.shadowColor = c[2]; ctx.shadowBlur = 20;
ctx.beginPath(); ctx.moveTo(bx, 0);
var curX = bx, curY = 0;
for (var s = 0; s < segments; s++) {
curX += (boltRng() - 0.5) * 60;
curY += h * 0.7 / segments + boltRng() * 20;
ctx.lineTo(curX, curY);
// Branch
if (boltRng() > 0.6) {
var branchLen = 20 + boltRng() * 40;
ctx.moveTo(curX, curY);
ctx.lineTo(curX + (boltRng() - 0.5) * branchLen, curY + branchLen * 0.5);
ctx.moveTo(curX, curY);
}
}
ctx.stroke();
ctx.shadowBlur = 0;
// Sky flash
ctx.fillStyle = H.rgba(c[2], 0.06);
ctx.fillRect(0, 0, w, h);
// Cloud underlight
for (var cl = 0; cl < 4; cl++) {
Draw.glow(ctx, w * 0.2 + boltRng() * w * 0.6, h * 0.1 + boltRng() * h * 0.15, 80, H.rgba(c[2], 0.15));
}
}
// Rain streaks
for (var r = 0; r < 30; r++) {
var rx = rng() * w;
var ry = rng() * h;
ctx.strokeStyle = H.rgba('#8899bb', 0.15);
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo(rx, ry); ctx.lineTo(rx - 3, ry + 15); ctx.stroke();
}
},
particles: { type: 'rain', count: 180, color: '#8899cc' }
},
aurora: {
meta: { id: 'weather_aurora', mood: 'magical', colors: ['#0a0a1a', '#0d1b2a', '#22cc88', '#8844dd'], tags: ['aurora', 'northern-lights', 'magical', 'night'], description: 'Dark arctic sky with shimmering aurora borealis in green and purple', compatibleMusicMoods: ['magical', 'dreamy', 'ambient', 'mysterious'], recommendedFor: ['creative', 'rpg', 'story', 'music'] },
colors: ['#0a0a1a', '#0d1b2a', '#22cc88', '#8844dd'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Stars
Draw.stars(ctx, w, h * 0.5, 80, '#FFFFFF', 881, [0.5, 2]);
},
renderMid: function(ctx, w, h, c, Draw, t) {
// Aurora bands
for (var band = 0; band < 5; band++) {
var bandY = h * 0.15 + band * h * 0.08;
var bandColor = band % 2 === 0 ? c[2] : c[3];
ctx.beginPath();
ctx.moveTo(0, bandY);
for (var x = 0; x <= w; x += 3) {
var wave1 = Math.sin(x * 0.005 + t * 0.3 + band * 0.8) * 25;
var wave2 = Math.sin(x * 0.012 + t * 0.15 + band * 1.2) * 15;
var wave3 = Math.sin(x * 0.002 + t * 0.08) * 35;
ctx.lineTo(x, bandY + wave1 + wave2 + wave3);
}
ctx.lineTo(w, bandY + h * 0.12);
ctx.lineTo(w, bandY);
for (var x2 = w; x2 >= 0; x2 -= 3) {
var w1 = Math.sin(x2 * 0.005 + t * 0.3 + band * 0.8) * 25;
var w2 = Math.sin(x2 * 0.012 + t * 0.15 + band * 1.2) * 15;
var w3 = Math.sin(x2 * 0.002 + t * 0.08) * 35;
ctx.lineTo(x2, bandY + w1 + w2 + w3 + h * 0.06);
}
ctx.closePath();
var alpha = 0.06 + Math.sin(t * 0.4 + band) * 0.03;
ctx.fillStyle = H.rgba(bandColor, alpha);
ctx.fill();
}
// Vertical aurora curtains
for (var cur = 0; cur < 8; cur++) {
var cx = w * 0.1 + cur * w * 0.1 + Math.sin(t * 0.2 + cur) * 20;
var curtainAlpha = 0.03 + Math.sin(t * 0.5 + cur * 1.3) * 0.02;
var cColor = cur % 3 === 0 ? c[2] : (cur % 3 === 1 ? c[3] : H.blendColor(c[2], c[3], 0.5));
ctx.fillStyle = H.rgba(cColor, curtainAlpha);
ctx.fillRect(cx, h * 0.05, 15 + Math.sin(t * 0.3 + cur) * 5, h * 0.45);
}
// Snow-covered ground
Draw.hills(ctx, w, h, h * 0.8, 5, '#1a2a3a', 882);
Draw.rect(ctx, 0, h * 0.85, w, h * 0.15, '#0d1520');
// Reflection on ground
for (var ref = 0; ref < 3; ref++) {
var refX = w * 0.2 + ref * w * 0.25;
var refColor = ref % 2 === 0 ? c[2] : c[3];
var refAlpha = 0.03 + Math.sin(t * 0.4 + ref) * 0.015;
Draw.glow(ctx, refX, h * 0.88, 60, H.rgba(refColor, refAlpha));
}
// Bright star
var starPulse = 0.6 + Math.sin(t * 1.5) * 0.3;
Draw.circle(ctx, w * 0.8, h * 0.08, 2, H.rgba('#FFFFFF', starPulse));
Draw.glow(ctx, w * 0.8, h * 0.08, 10, '#FFFFFF');
},
particles: { type: 'stars', count: 60, color: '#FFFFFF' }
}
});
})(window.BackgroundEngine);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,168 @@
// moodCategories.js — Mood category definitions for procedural music system
// Loaded by HTML5 games via <script> tag; defines musical characteristics per mood
(function() {
'use strict';
// ─── Mood Definitions ──────────────────────────────────────
//
// Each mood defines the musical personality for procedural generation:
// name — Display name
// description — Human-readable summary of the mood's feel
// energyRange — [min, max] energy level (1-10 scale)
// tempoRange — [min, max] BPM
// preferredScales — Scale types that suit this mood
// preferredSynths — Synth types per voice role (lead, pad, bass)
// typicalTags — Tags for matching moods to game contexts
// color — Hex color for UI display
const MOODS = {
Epic: {
name: 'Epic',
description: 'Grand, dramatic, triumphant music for boss fights and climactic moments',
energyRange: [7, 10],
tempoRange: [120, 170],
preferredScales: ['minor', 'harmonicMinor', 'dorian'],
preferredSynths: { lead: ['FM', 'Poly'], pad: ['Poly', 'AM'], bass: ['Mono', 'FM'] },
typicalTags: ['action', 'boss-fight', 'heroic', 'battle', 'climax', 'victory', 'dramatic', 'powerful'],
color: '#ff4444',
},
Chill: {
name: 'Chill',
description: 'Relaxed, smooth music for menus, idle screens, and peaceful moments',
energyRange: [1, 4],
tempoRange: [70, 110],
preferredScales: ['major', 'lydian', 'pentatonicMajor'],
preferredSynths: { lead: ['AM', 'Poly'], pad: ['Poly', 'AM'], bass: ['Mono', 'AM'] },
typicalTags: ['relax', 'lounge', 'calm', 'peaceful', 'meditation', 'smooth', 'easy', 'soft'],
color: '#44bbff',
},
Intense: {
name: 'Intense',
description: 'Aggressive, driving music for chases, danger zones, and high-stakes action',
energyRange: [8, 10],
tempoRange: [140, 180],
preferredScales: ['minor', 'phrygian', 'blues'],
preferredSynths: { lead: ['FM', 'Mono'], pad: ['FM', 'Mono'], bass: ['Mono', 'FM'] },
typicalTags: ['chase', 'danger', 'urgent', 'fast', 'aggressive', 'fierce', 'thrilling', 'adrenaline'],
color: '#ff2222',
},
Playful: {
name: 'Playful',
description: 'Bouncy, fun music for lighthearted gameplay and cheerful environments',
energyRange: [4, 7],
tempoRange: [110, 150],
preferredScales: ['major', 'mixolydian', 'pentatonicMajor'],
preferredSynths: { lead: ['Pluck', 'FM'], pad: ['Pluck', 'FM'], bass: ['FM', 'Pluck'] },
typicalTags: ['fun', 'bouncy', 'happy', 'silly', 'cartoon', 'cheerful', 'lighthearted', 'dance'],
color: '#ffcc00',
},
Mysterious: {
name: 'Mysterious',
description: 'Dark, atmospheric music for puzzles, suspense, and shadowy exploration',
energyRange: [2, 5],
tempoRange: [80, 120],
preferredScales: ['minor', 'phrygian', 'harmonicMinor', 'wholeTone'],
preferredSynths: { lead: ['AM', 'Poly'], pad: ['Poly', 'AM'], bass: ['Mono', 'AM'] },
typicalTags: ['mystery', 'dark', 'suspense', 'eerie', 'detective', 'fog', 'shadow', 'puzzle'],
color: '#9944cc',
},
Heroic: {
name: 'Heroic',
description: 'Triumphant, bold music for adventures, quests, and moments of glory',
energyRange: [6, 9],
tempoRange: [120, 160],
preferredScales: ['major', 'lydian', 'mixolydian'],
preferredSynths: { lead: ['FM', 'Poly'], pad: ['Poly', 'FM'], bass: ['Mono', 'FM'] },
typicalTags: ['hero', 'triumph', 'brave', 'adventure', 'quest', 'champion', 'glory', 'courage'],
color: '#ffaa22',
},
Quirky: {
name: 'Quirky',
description: 'Unpredictable, comedic music for offbeat humor and eccentric characters',
energyRange: [4, 8],
tempoRange: [100, 140],
preferredScales: ['mixolydian', 'dorian', 'blues', 'wholeTone'],
preferredSynths: { lead: ['FM', 'AM'], pad: ['AM', 'Pluck'], bass: ['FM', 'Pluck'] },
typicalTags: ['weird', 'funny', 'comedy', 'offbeat', 'eccentric', 'surprise', 'wacky', 'odd'],
color: '#44dd88',
},
Ambient: {
name: 'Ambient',
description: 'Atmospheric, spacious music for dreamlike environments and cosmic exploration',
energyRange: [1, 3],
tempoRange: [60, 90],
preferredScales: ['major', 'minor', 'pentatonicMajor', 'pentatonicMinor'],
preferredSynths: { lead: ['Poly', 'AM'], pad: ['Poly', 'AM'], bass: ['AM', 'Poly'] },
typicalTags: ['space', 'atmosphere', 'dream', 'floating', 'ethereal', 'vast', 'tranquil', 'cosmic'],
color: '#6688cc',
},
};
// ─── Helpers ───────────────────────────────────────────────
/** All mood names as a frozen array */
const moodNames = Object.freeze(Object.keys(MOODS));
// ─── Public API ────────────────────────────────────────────
window.MoodCategories = {
/**
* Get a mood definition by name.
* @param {string} name Mood name (e.g. 'Epic', 'Chill')
* @returns {object|null} The mood object, or null if not found
*/
getMood(name) {
return MOODS[name] || null;
},
/**
* Get all mood definitions as a frozen object.
* @returns {object} Map of mood name -> mood definition
*/
getAllMoods() {
return MOODS;
},
/**
* Get all mood names.
* @returns {string[]} Array of mood names
*/
getMoodNames() {
return moodNames;
},
/**
* Get a random mood definition.
* @returns {object} A randomly selected mood object
*/
getRandomMood() {
const name = moodNames[Math.floor(Math.random() * moodNames.length)];
return MOODS[name];
},
/**
* Get all moods whose energy range overlaps with the given [min, max].
* @param {number} min Minimum energy level (1-10)
* @param {number} max Maximum energy level (1-10)
* @returns {object[]} Array of matching mood objects
*/
getMoodsByEnergy(min, max) {
return moodNames
.map(name => MOODS[name])
.filter(mood => mood.energyRange[0] <= max && mood.energyRange[1] >= min);
},
};
})();
+848
View File
@@ -0,0 +1,848 @@
// musicEngine.js — Procedural music engine using Tone.js
// Loaded by HTML5 games via <script> tag; reads song definitions from MusicLibrary
// Global: window.MusicEngine
(function() {
'use strict';
// ─── Constants ───────────────────────────────────────────
var NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
var SCALES = {
major: [0, 2, 4, 5, 7, 9, 11],
minor: [0, 2, 3, 5, 7, 8, 10],
dorian: [0, 2, 3, 5, 7, 9, 10],
phrygian: [0, 1, 3, 5, 7, 8, 10],
lydian: [0, 2, 4, 6, 7, 9, 11],
mixolydian: [0, 2, 4, 5, 7, 9, 10],
harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
melodicMinor: [0, 2, 3, 5, 7, 9, 11],
pentatonicMajor: [0, 2, 4, 7, 9],
pentatonicMinor: [0, 3, 5, 7, 10],
blues: [0, 3, 5, 6, 7, 10],
wholeTone: [0, 2, 4, 6, 8, 10],
chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
};
// Roman numeral to scale degree (0-indexed)
var ROMAN = { i: 0, ii: 1, iii: 2, iv: 3, v: 4, vi: 5, vii: 6 };
// Short synth type names to Tone.js constructor names
var SYNTH_MAP = {
FM: 'FMSynth', AM: 'AMSynth', Mono: 'MonoSynth', Poly: 'PolySynth',
Membrane: 'MembraneSynth', Metal: 'MetalSynth', Pluck: 'PluckSynth',
Duo: 'DuoSynth', Noise: 'NoiseSynth',
};
// ─── Seeded PRNG ─────────────────────────────────────────
function hashStr(s) {
var h = 0;
for (var i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
return Math.abs(h) || 1;
}
function seededRandom(seed) {
var s = hashStr(String(seed));
return function() {
s = (s * 16807) % 2147483647;
return (s - 1) / 2147483646;
};
}
// ─── Note Utilities ──────────────────────────────────────
function noteToMidi(n) {
var m = n.match(/^([A-G]#?)(\d+)$/);
return m ? (parseInt(m[2]) + 1) * 12 + NOTES.indexOf(m[1]) : 60;
}
function midiToNote(m) {
return NOTES[m % 12] + (Math.floor(m / 12) - 1);
}
function getScaleNotes(root, scale, octave) {
var ri = NOTES.indexOf(root);
if (ri < 0) return [];
return (SCALES[scale] || SCALES.major).map(function(semi) {
return midiToNote((octave + 1) * 12 + ri + semi);
});
}
function multiOctave(root, scale, lo, hi) {
var notes = [];
for (var o = lo; o <= hi; o++) notes = notes.concat(getScaleNotes(root, scale, o));
return notes;
}
// ─── Chord Parsing ───────────────────────────────────────
function parseChords(str, key, scale) {
if (!str || !str.trim()) return [];
var ri = NOTES.indexOf(key), intervals = SCALES[scale] || SCALES.major;
return str.trim().split(/\s+/).map(function(sym) {
var s = sym, sf = 0;
// Check for accidentals prefix (b or #)
if (s[0] === 'b') { sf = -1; s = s.slice(1); }
else if (s[0] === '#') { sf = 1; s = s.slice(1); }
// Check for 7th suffix
var has7 = s.endsWith('7');
if (has7) s = s.slice(0, -1);
// Check for diminished suffix
var dim = s.endsWith('\u00b0');
if (dim) s = s.slice(0, -1);
// Uppercase = major triad, lowercase = minor triad
var isUpper = s[0] === s[0].toUpperCase();
var deg = ROMAN[s.toLowerCase()];
if (deg === undefined) deg = 0;
var chordRoot = (ri + (intervals[deg % intervals.length] || 0) + sf + 12) % 12;
var baseMidi = 48 + chordRoot; // octave 3
var tones = dim ? [baseMidi, baseMidi + 3, baseMidi + 6]
: isUpper ? [baseMidi, baseMidi + 4, baseMidi + 7]
: [baseMidi, baseMidi + 3, baseMidi + 7];
if (has7) tones.push(isUpper ? baseMidi + 11 : baseMidi + 10);
return {
symbol: sym, root: midiToNote(baseMidi), rootMidi: baseMidi,
notes: tones.map(midiToNote), midiNotes: tones,
};
});
}
// ─── Event shorthand constructors ────────────────────────
function E(note, dur, vel, time) { return { note: note, duration: dur, velocity: vel, time: time }; }
function D(voice, time, vel) { return { voice: voice, time: time, velocity: vel }; }
// ─── Melody Generators ───────────────────────────────────
// Each: (scaleNotes, chord, intensity, barNum, prev, rng) -> [{note, duration, velocity, time}]
var melGen = {
'arp-up': function(sn, c, I) {
var n = [c.midiNotes[0], c.midiNotes[1], c.midiNotes[2], c.midiNotes[0] + 12];
return n.map(function(m, i) { return E(midiToNote(m), '8n', 0.5 + I * 0.4, i * 0.5); });
},
'arp-down': function(sn, c, I) {
var n = [c.midiNotes[0] + 12, c.midiNotes[2], c.midiNotes[1], c.midiNotes[0]];
return n.map(function(m, i) { return E(midiToNote(m), '8n', 0.5 + I * 0.4, i * 0.5); });
},
'arp-bounce': function(sn, c, I) {
var t = c.midiNotes, s = [t[0], t[1], t[2], t[1], t[0], t[2], t[1], t[0]];
return s.map(function(m, i) { return E(midiToNote(m), '8n', 0.45 + I * 0.4, i * 0.5); });
},
'stepwise': function(sn, c, I, b, p, rng) {
var ev = [], si = Math.max(0, Math.floor(sn.length * 0.3)), t = 0;
for (var i = 0; i < 6 && t < 4; i++) {
var d = rng() > 0.5 ? 0.5 : 1;
ev.push(E(sn[Math.min(si + i, sn.length - 1)], d > 0.75 ? '4n' : '8n', 0.5 + I * 0.35, t));
t += d;
}
return ev;
},
'leap': function(sn, c, I, b, p, rng) {
var ev = [], idx = Math.floor(sn.length * 0.3), t = 0;
for (var i = 0; i < 4 && t < 4; i++) {
ev.push(E(sn[Math.min(idx, sn.length - 1)], '4n', 0.55 + I * 0.35, t));
idx += Math.floor(rng() * 3) + 2;
if (idx >= sn.length) idx = Math.floor(sn.length * 0.3);
t += rng() > 0.3 ? 1 : 0.5;
}
return ev;
},
'sustained': function(sn, c, I) {
return [E(c.notes[0], '2n', 0.4 + I * 0.3, 0), E(c.notes[2] || c.notes[1], '2n', 0.35 + I * 0.3, 2)];
},
'rhythmic': function(sn, c, I, b, p, rng) {
var ev = [], r = c.notes[0];
for (var i = 0; i < 16; i++) {
if (rng() < 0.5 + I * 0.3)
ev.push(E(r, '16n', (i % 4 === 0 ? 0.7 : 0.4) * (0.5 + I * 0.5), i * 0.25));
}
return ev;
},
'call-response': function(sn, c, I) {
var h = Math.min(Math.floor(sn.length * 0.7), sn.length - 1), l = Math.floor(sn.length * 0.3);
return [
E(sn[h], '8n', 0.6 + I * 0.3, 0),
E(sn[Math.min(h + 1, sn.length - 1)], '8n', 0.55 + I * 0.3, 0.5),
E(sn[l], '8n', 0.5 + I * 0.3, 2),
E(sn[Math.max(l - 1, 0)], '4n', 0.5 + I * 0.3, 2.5),
];
},
'pentatonic': function(sn, c, I, b, p, rng) {
var ri = NOTES.indexOf(c.root.replace(/\d+/, ''));
var pn = SCALES.pentatonicMinor.map(function(s) { return midiToNote(48 + ri + s); });
var ev = [], t = 0;
while (t < 4) { ev.push(E(pn[Math.floor(rng() * pn.length)], '8n', 0.45 + I * 0.4, t)); t += 0.5; }
return ev;
},
'ostinato': function(sn, c, I) {
var si = 0;
for (var i = 0; i < sn.length; i++) if (noteToMidi(sn[i]) >= c.rootMidi) { si = i; break; }
var motif = [];
for (var j = 0; j < Math.min(4, sn.length - si); j++) motif.push(sn[si + j]);
var ev = [];
for (var b = 0; b < 4; b++) ev.push(E(motif[b % motif.length], '8n', 0.5 + I * 0.35, b));
return ev;
},
'descending': function(sn, c, I) {
var s = Math.min(Math.floor(sn.length * 0.7), sn.length - 1), ev = [], t = 0;
for (var i = s; i >= 0 && t < 4; i--, t += 0.5) ev.push(E(sn[i], '8n', 0.5 + I * 0.35, t));
return ev;
},
'ascending': function(sn, c, I) {
var s = Math.floor(sn.length * 0.2), ev = [], t = 0;
for (var i = s; i < sn.length && t < 4; i++, t += 0.5) ev.push(E(sn[i], '8n', 0.5 + I * 0.35, t));
return ev;
},
'chromatic': function(sn, c, I) {
var ev = [], m = c.rootMidi, t = 0;
for (var i = 0; i < 8 && t < 4; i++) {
ev.push(E(midiToNote(m), '8n', 0.5 + I * 0.3, t));
m += (i % 2 === 0) ? 1 : 2; t += 0.5;
}
return ev;
},
'fade': function(sn, c, I, bar) {
var ev = [], cnt = Math.max(1, 8 - bar * 2);
for (var i = 0; i < cnt && i * 0.5 < 4; i++)
ev.push(E(c.notes[i % c.notes.length], '4n', Math.max(0.1, (0.5 + I * 0.3) * (1 - i / 8)), i * 0.5));
return ev;
},
'trill': function(sn, c, I) {
var idx = 0;
for (var i = 0; i < sn.length; i++) if (noteToMidi(sn[i]) >= c.rootMidi) { idx = i; break; }
var n1 = sn[idx], n2 = sn[Math.min(idx + 1, sn.length - 1)], ev = [];
for (var t = 0; t < 4; t += 0.25)
ev.push(E(Math.round(t / 0.25) % 2 === 0 ? n1 : n2, '16n', 0.4 + I * 0.35, t));
return ev;
},
'fanfare': function(sn, c, I) {
var v = 0.6 + I * 0.25, V = 0.7 + I * 0.25;
return [E(c.notes[0], '4n.', V, 0), E(c.notes[1] || c.notes[0], '8n', v, 1.5),
E(c.notes[2] || c.notes[0], '4n.', V, 2), E(c.notes[0], '8n', v, 3.5)];
},
'blues': function(sn, c, I, b, p, rng) {
var rm = c.rootMidi, bn = [rm, rm + 3, rm + 5, rm + 6, rm + 7, rm + 10], ev = [], t = 0;
while (t < 4) {
var sw = (Math.round(t * 2) % 2 === 1) ? 0.08 : 0;
ev.push(E(midiToNote(bn[Math.floor(rng() * bn.length)]), '8n', 0.5 + I * 0.4, t + sw));
t += 0.5;
}
return ev;
},
'staccato': function(sn, c, I, b, p, rng) {
var ev = [];
for (var t = 0; t < 4; t += 0.5)
if (rng() < 0.7) ev.push(E(sn[Math.floor(rng() * sn.length)], '16n', 0.5 + I * 0.4, t));
return ev;
},
'legato': function(sn, c, I) {
var s = Math.floor(sn.length * 0.3);
return [E(sn[s], '2n', 0.45 + I * 0.3, 0),
E(sn[Math.min(s + 2, sn.length - 1)], '4n', 0.45 + I * 0.3, 2),
E(sn[Math.min(s + 4, sn.length - 1)], '4n', 0.4 + I * 0.3, 3)];
},
'random': function(sn, c, I, b, p, rng) {
var ev = [], t = 0, ds = [0.25, 0.5, 1], dn = ['16n', '8n', '4n'];
while (t < 4) {
var n = rng() < 0.5 ? c.notes[Math.floor(rng() * c.notes.length)] : sn[Math.floor(rng() * sn.length)];
var di = Math.floor(rng() * 3);
ev.push(E(n, dn[di], 0.4 + I * 0.4 * rng(), t));
t += ds[di];
}
return ev;
},
'silence': function() { return []; },
};
// ─── Bass Generators ─────────────────────────────────────
// Each: (chord, chordTones, scaleNotes, intensity, rng) -> [{note, duration, velocity, time}]
var bassGen = {
'root': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), v = 0.6 + I * 0.3, v2 = 0.55 + I * 0.3;
return [E(r, '4n', v, 0), E(r, '4n', v2, 1), E(r, '4n', v, 2), E(r, '4n', v2, 3)];
},
'root-fifth': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), f = midiToNote(c.rootMidi - 5), v = 0.6 + I * 0.3;
return [E(r, '4n', v, 0), E(f, '4n', v - 0.05, 1), E(r, '4n', v, 2), E(f, '4n', v - 0.05, 3)];
},
'driving': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), ev = [];
for (var t = 0; t < 4; t += 0.5)
ev.push(E(r, '8n', (t % 1 === 0 ? 0.65 : 0.5) + I * 0.25, t));
return ev;
},
'walking': function(c, _, __, I) {
var b = c.rootMidi - 12;
return [b, b + 2, b + 4, b + 5].map(function(m, i) {
return E(midiToNote(m), '4n', 0.55 + I * 0.3, i);
});
},
'arpeggiated': function(c) {
var a = c.midiNotes.map(function(m) { return m - 12; }), ev = [], t = 0;
for (var i = 0; t < 4; i++, t += 0.5)
ev.push(E(midiToNote(a[i % a.length]), '8n', 0.55, t));
return ev;
},
'octave': function(c, _, __, I) {
var lo = midiToNote(c.rootMidi - 24), hi = midiToNote(c.rootMidi - 12), v = 0.6 + I * 0.3;
return [E(lo, '4n', v, 0), E(hi, '4n', v - 0.05, 1), E(lo, '4n', v, 2), E(hi, '4n', v - 0.05, 3)];
},
'syncopated': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), v = 0.6 + I * 0.25;
return [E(r, '4n', v + 0.05, 0), E(r, '8n', v - 0.05, 1.5), E(r, '4n', v - 0.05, 2.5)];
},
'pedal': function(c) {
return [E(midiToNote(c.rootMidi - 12), '1n', 0.55, 0)];
},
'pumping': function(c, _, __, I) {
var r = midiToNote(c.rootMidi - 12), ev = [];
for (var t = 0; t < 4; t += 0.5)
ev.push(E(r, '8n', 0.5 + I * 0.25 + ((t === 0 || t === 2) ? 0.15 : 0), t));
return ev;
},
'fifths': function(c, _, __, I) {
var r = c.rootMidi - 12, f = r + 7, p = r + 4, v = 0.6 + I * 0.3;
return [E(midiToNote(r), '4n', v, 0), E(midiToNote(f), '8n', v - 0.05, 1),
E(midiToNote(p), '8n', v - 0.1, 1.5), E(midiToNote(r), '4n', v, 2),
E(midiToNote(f), '4n', v - 0.05, 3)];
},
'chromatic': function(c) {
var b = c.rootMidi - 12;
return [E(midiToNote(b - 2), '4n', 0.5, 0), E(midiToNote(b - 1), '4n', 0.55, 1),
E(midiToNote(b), '2n', 0.6, 2)];
},
'silence': function() { return []; },
};
// ─── Drum Generators ─────────────────────────────────────
// Each: (intensity, timeSig, barNum) -> [{voice, time, velocity}]
var drumGen = {
'standard': function(I) {
var e = [D('kick', 0, 0.8), D('kick', 2, 0.75), D('snare', 1, 0.7), D('snare', 3, 0.7)];
for (var t = 0; t < 4; t += 0.5)
e.push(D('hihat', t, (t % 1 === 0 ? 0.4 : 0.25) * (0.5 + I * 0.5)));
return e;
},
'four-on-floor': function(I) {
var e = [D('snare', 1, 0.65), D('snare', 3, 0.65)];
for (var b = 0; b < 4; b++) e.push(D('kick', b, 0.8));
for (var t = 0; t < 4; t += 0.5)
e.push(D('hihat', t, (t % 1 === 0 ? 0.35 : 0.2) * (0.5 + I * 0.5)));
return e;
},
'half-time': function() {
return [D('kick', 0, 0.8), D('snare', 2, 0.75), D('hihat', 0, 0.3), D('hihat', 2, 0.25)];
},
'breakbeat': function() {
var e = [D('kick', 0, 0.8), D('kick', 1.5, 0.7), D('kick', 3, 0.75),
D('snare', 1, 0.7), D('snare', 3, 0.7)];
for (var t = 0; t < 4; t += 0.5)
e.push(D('hihat', t, (t % 1 === 0 ? 0.3 : 0.2) + (t === 1.5 ? 0.15 : 0)));
return e;
},
'shuffle': function() {
var e = [D('kick', 0, 0.8), D('kick', 2, 0.75), D('snare', 1, 0.7), D('snare', 3, 0.7)];
for (var b = 0; b < 4; b++) { e.push(D('hihat', b, 0.35)); e.push(D('hihat', b + 0.67, 0.2)); }
return e;
},
'full': function(I) {
var e = [D('kick', 0, 0.85), D('kick', 1.5, 0.6), D('kick', 2, 0.8), D('kick', 3.5, 0.55),
D('snare', 1, 0.75), D('snare', 3, 0.75), D('tom', 2.5, 0.5), D('crash', 0, 0.3)];
for (var t = 0; t < 4; t += 0.25)
e.push(D('hihat', t, (t % 0.5 === 0 ? 0.3 : 0.15) * (0.5 + I * 0.5)));
return e;
},
'sparse': function() {
return [D('kick', 0, 0.7), D('snare', 2, 0.55)];
},
'build': function(I, ts, bar) {
var e = [D('kick', 0, 0.7)], density = Math.min(1, (bar || 0) * 0.15 + 0.1);
if (density > 0.2) e.push(D('kick', 2, 0.65));
if (density > 0.4) { e.push(D('snare', 1, 0.5)); e.push(D('snare', 3, 0.5)); }
if (density > 0.6) for (var t = 0; t < 4; t += 0.5) e.push(D('hihat', t, 0.25));
if (density > 0.8) e.push(D('crash', 0, 0.35));
return e;
},
'tribal': function() {
return [D('tom', 0, 0.8), D('tom', 0.75, 0.5), D('tom', 1.5, 0.65), D('kick', 2, 0.75),
D('tom', 2.5, 0.6), D('tom', 3, 0.7), D('tom', 3.5, 0.45)];
},
'double-time': function(I) {
var e = [];
for (var t = 0; t < 4; t += 0.5) e.push(D('kick', t, t % 1 === 0 ? 0.75 : 0.5));
for (var s = 0.5; s < 4; s += 1) e.push(D('snare', s, 0.6));
for (var h = 0; h < 4; h += 0.25) e.push(D('hihat', h, 0.2 + I * 0.15));
return e;
},
'kick-only': function() {
return [D('kick', 0, 0.75), D('kick', 1, 0.7), D('kick', 2, 0.75), D('kick', 3, 0.7)];
},
'hats-only': function(I) {
var e = [], step = I > 0.7 ? 0.25 : 0.5;
for (var t = 0; t < 4; t += step) e.push(D('hihat', t, t % 1 === 0 ? 0.4 : 0.25));
return e;
},
'march': function(I) {
var e = [D('kick', 0, 0.8), D('kick', 2, 0.75)];
for (var b = 0; b < 4; b++) {
e.push(D('snare', b, 0.65));
if (I > 0.5) { e.push(D('snare', b + 0.25, 0.35)); e.push(D('snare', b + 0.5, 0.3)); }
}
return e;
},
'waltz': function() {
return [D('kick', 0, 0.8), D('hihat', 0.67, 0.35), D('hihat', 1.33, 0.3),
D('kick', 2, 0.7), D('hihat', 2.67, 0.35), D('hihat', 3.33, 0.3)];
},
'none': function() { return []; },
};
// ─── Engine State ────────────────────────────────────────
var Tone = null;
var initialized = false;
var currentSong = null;
var playing = false;
var masterGain = null;
var currentNodes = []; // all disposable Tone.js nodes for current song
var currentParts = []; // scheduled parts for cleanup
var loopCbId = null;
// ─── Tone.js CDN Loading ─────────────────────────────────
async function loadToneJs() {
if (window.Tone) return window.Tone;
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = 'https://unpkg.com/tone@15.1.22/build/Tone.js';
script.onload = function() { resolve(window.Tone); };
script.onerror = function() { reject(new Error('Failed to load Tone.js')); };
document.head.appendChild(script);
});
}
// ─── Synth Creation ──────────────────────────────────────
var ROLE_DEFAULTS = {
lead: { envelope: { attack: 0.03, decay: 0.2, sustain: 0.8, release: 0.4 }, volume: -6 },
pad: { envelope: { attack: 0.5, decay: 0.3, sustain: 0.9, release: 2.0 }, volume: -12 },
bass: { envelope: { attack: 0.01, decay: 0.15, sustain: 0.7, release: 0.2 }, volume: -8 },
};
function createSynth(typeName, role, custom) {
var ctorName = SYNTH_MAP[typeName] || 'Synth';
var Ctor = Tone[ctorName];
if (!Ctor) Ctor = Tone.Synth;
var settings = Object.assign({}, ROLE_DEFAULTS[role] || {}, custom || {});
// PolySynth wraps an inner synth type
if (ctorName === 'PolySynth') return new Tone.PolySynth(Tone.Synth, settings);
// These synth types have non-standard constructors
if (ctorName === 'PluckSynth' || ctorName === 'NoiseSynth' || ctorName === 'MetalSynth') {
try { return new Ctor(custom || {}); } catch (e) { return new Ctor(); }
}
try { return new Ctor(settings); } catch (e) { return new Ctor(); }
}
// ─── Drum Kit ────────────────────────────────────────────
function createDrumKit() {
return {
kick: new Tone.MembraneSynth({ pitchDecay: 0.05, octaves: 6,
envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.4 }, volume: -6 }),
snare: new Tone.NoiseSynth({ noise: { type: 'white' },
envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.1 }, volume: -10 }),
hihat: new Tone.MetalSynth({ frequency: 400,
envelope: { attack: 0.001, decay: 0.05, release: 0.01 },
harmonicity: 5.1, modulationIndex: 32, resonance: 4000, octaves: 1.5, volume: -18 }),
tom: new Tone.MembraneSynth({ pitchDecay: 0.08, octaves: 4,
envelope: { attack: 0.001, decay: 0.25, sustain: 0, release: 0.3 }, volume: -8 }),
crash: new Tone.NoiseSynth({ noise: { type: 'white' },
envelope: { attack: 0.001, decay: 0.8, sustain: 0, release: 0.5 }, volume: -16 }),
};
}
// ─── Effects Chain ───────────────────────────────────────
// instruments -> filter -> [chorus] -> [distortion] -> delay -> reverb -> destination
function createFxChain(fx) {
var nodes = [], chain = [];
var filter = new Tone.Filter({ frequency: fx.filter || 5000, type: 'lowpass' });
nodes.push(filter); chain.push(filter);
if (fx.chorus > 0) {
var chorus = new Tone.Chorus({ frequency: 1.5, delayTime: 3.5, depth: 0.7, wet: fx.chorus });
chorus.start();
nodes.push(chorus); chain.push(chorus);
}
if (fx.distortion > 0) {
var dist = new Tone.Distortion({ distortion: fx.distortion, wet: fx.distortion });
nodes.push(dist); chain.push(dist);
}
var delay = new Tone.FeedbackDelay({ delayTime: '8n', feedback: 0.3, wet: fx.delay || 0 });
nodes.push(delay); chain.push(delay);
var reverb = new Tone.Reverb({ decay: 2.5, wet: fx.reverb || 0 });
nodes.push(reverb); chain.push(reverb);
return { nodes: nodes, chain: chain };
}
// ─── Cleanup ─────────────────────────────────────────────
function disposeAll() {
if (Tone) {
try { Tone.getTransport().cancel(0); } catch (e) {}
try { Tone.getTransport().stop(); } catch (e) {}
}
currentParts.forEach(function(p) { try { p.dispose(); } catch (e) {} });
currentParts = [];
currentNodes.forEach(function(n) { try { if (n && n.dispose) n.dispose(); } catch (e) {} });
currentNodes = [];
if (loopCbId !== null && Tone) {
try { Tone.getTransport().clear(loopCbId); } catch (e) {}
loopCbId = null;
}
}
// ─── Song Library Access ─────────────────────────────────
function getSongs() {
return (window.MusicLibrary && Array.isArray(window.MusicLibrary.songs))
? window.MusicLibrary.songs : [];
}
// ─── Song Scheduling ────────────────────────────────────
function scheduleSong(song) {
var transport = Tone.getTransport();
transport.cancel(0);
transport.stop();
transport.bpm.value = song.tempo || 120;
transport.position = 0;
var rng = seededRandom(song.id || 'default');
var key = song.key || 'C', scale = song.scale || 'minor';
var scaleNotes = multiOctave(key, scale, 3, 5);
// Create effects chain
var fx = Object.assign({ reverb: 0.2, delay: 0.1, filter: 5000, chorus: 0, distortion: 0 }, song.fx || {});
var effects = createFxChain(fx);
currentNodes = currentNodes.concat(effects.nodes);
// Master gain -> destination
masterGain = new Tone.Gain(0.7).toDestination();
currentNodes.push(masterGain);
// Wire effects chain: each node connects to the next, last connects to master gain
for (var ci = 0; ci < effects.chain.length - 1; ci++)
effects.chain[ci].connect(effects.chain[ci + 1]);
effects.chain[effects.chain.length - 1].connect(masterGain);
// Create melodic synths and route through effects
var synths = song.synths || { lead: 'FM', pad: 'Poly', bass: 'Mono' };
var ss = song.synthSettings || {};
var lead = createSynth(synths.lead, 'lead', ss.lead);
var pad = createSynth(synths.pad, 'pad', ss.pad);
var bass = createSynth(synths.bass, 'bass', ss.bass);
[lead, pad, bass].forEach(function(s) { currentNodes.push(s); s.connect(effects.chain[0]); });
// Create drum kit and route directly to master gain (no effects)
var drums = createDrumKit();
['kick', 'snare', 'hihat', 'tom', 'crash'].forEach(function(k) {
currentNodes.push(drums[k]);
drums[k].connect(masterGain);
});
// Build the complete event list from the song form
var form = song.form || ['verse', 'chorus'];
var sections = song.sections || {};
var allEvts = [], barOffset = 0;
for (var fi = 0; fi < form.length; fi++) {
var sec = sections[form[fi]];
if (!sec) continue;
var nBars = sec[0] || 4;
var chStr = sec[1] || 'i';
var mPat = sec[2] || 'stepwise';
var bPat = sec[3] || 'root';
var dPat = sec[4] || 'standard';
var intensity = sec[5] !== undefined ? sec[5] : 0.5;
var chords = parseChords(chStr, key, scale);
if (!chords.length) chords = parseChords('i', key, scale);
var mG = melGen[mPat] || melGen.stepwise;
var bG = bassGen[bPat] || bassGen.root;
var dG = drumGen[dPat] || drumGen.standard;
for (var bar = 0; bar < nBars; bar++) {
var chord = chords[bar % chords.length];
var bt = (barOffset + bar) * 4; // bar time in beats
// Melody events
try {
mG(scaleNotes, chord, intensity, bar, null, rng).forEach(function(e) {
allEvts.push({ type: 'lead', note: e.note, duration: e.duration, velocity: e.velocity, time: bt + e.time });
});
} catch (e) {}
// Pad chords (whole notes on chord tones)
if (chord.notes && chord.notes.length >= 2) {
for (var cn = 0; cn < Math.min(chord.notes.length, 3); cn++)
allEvts.push({ type: 'pad', note: chord.notes[cn], duration: '1n', velocity: 0.25 + intensity * 0.2, time: bt });
}
// Bass events
try {
bG(chord, chord.notes, scaleNotes, intensity, rng).forEach(function(e) {
allEvts.push({ type: 'bass', note: e.note, duration: e.duration, velocity: e.velocity, time: bt + e.time });
});
} catch (e) {}
// Drum events
try {
dG(intensity, 4, bar).forEach(function(e) {
allEvts.push({ type: 'drum', voice: e.voice, velocity: e.velocity, time: bt + e.time });
});
} catch (e) {}
}
barOffset += nBars;
}
var totalSeconds = (barOffset * 4 / song.tempo) * 60;
// Schedule all events on the Tone.js Transport
allEvts.forEach(function(evt) {
var timeInSec = (evt.time / song.tempo) * 60;
transport.schedule(function(t) {
try {
if (evt.type === 'lead') {
lead.triggerAttackRelease(evt.note, evt.duration, t, evt.velocity);
} else if (evt.type === 'pad' && typeof pad.triggerAttackRelease === 'function') {
pad.triggerAttackRelease(evt.note, evt.duration, t, evt.velocity);
} else if (evt.type === 'bass') {
bass.triggerAttackRelease(evt.note, evt.duration, t, evt.velocity);
} else if (evt.type === 'drum') {
var inst = drums[evt.voice];
if (!inst) return;
if (evt.voice === 'kick' || evt.voice === 'tom')
inst.triggerAttackRelease(evt.voice === 'kick' ? 'C1' : 'G2', '8n', t, evt.velocity);
else if (evt.voice === 'snare' || evt.voice === 'crash')
inst.triggerAttackRelease('8n', t, evt.velocity);
else if (evt.voice === 'hihat')
inst.triggerAttackRelease('32n', t, evt.velocity);
}
} catch (err) { /* skip individual note errors */ }
}, timeInSec);
});
// Schedule loop: when song ends, dispose and re-schedule (skip intro on loop)
loopCbId = transport.schedule(function() {
transport.stop();
setTimeout(function() {
if (playing && currentSong && currentSong.id === song.id) {
disposeAll();
scheduleSong(song);
Tone.getTransport().start();
if (window.MusicEngine.onSongEnd) {
try { window.MusicEngine.onSongEnd(song); } catch (e) {}
}
}
}, 50);
}, totalSeconds);
return { totalSeconds: totalSeconds, totalBars: barOffset };
}
// ─── Metadata helper ─────────────────────────────────────
function meta(s) {
return { id: s.id, name: s.name, mood: s.mood, energy: s.energy, tempo: s.tempo, tags: s.tags, desc: s.desc };
}
// ─── Public API ──────────────────────────────────────────
window.MusicEngine = {
onSongEnd: null,
/** Initialize the engine. Must be called on a user gesture (click/touch). */
async init() {
if (initialized) return;
Tone = await loadToneJs();
await Tone.start();
initialized = true;
},
/** Play a random song matching the given mood. Returns the song object or null. */
async playRandomSong(mood) {
if (!initialized) await this.init();
var songs = getSongs().filter(function(s) {
return s.mood && s.mood.toLowerCase() === mood.toLowerCase();
});
if (!songs.length) return null;
return this.playSong(songs[Math.floor(Math.random() * songs.length)].id);
},
/** Play a specific song by ID. Returns the song object or null. */
async playSong(songId) {
if (!initialized) await this.init();
var song = null;
getSongs().forEach(function(s) { if (s.id === songId) song = s; });
if (!song) return null;
this.stopMusic();
try {
currentSong = song;
playing = true;
scheduleSong(song);
Tone.getTransport().start();
return song;
} catch (e) {
this.stopMusic();
return null;
}
},
/** Stop the current song immediately and clean up all Tone.js nodes. */
stopMusic() {
playing = false;
currentSong = null;
disposeAll();
},
/** Set master volume (0 = silent, 1 = full). */
setVolume(level) {
var v = Math.max(0, Math.min(1, level));
if (masterGain) masterGain.gain.value = v;
if (Tone && Tone.getDestination) {
try { Tone.getDestination().volume.value = v === 0 ? -Infinity : 20 * Math.log10(v); } catch (e) {}
}
},
/** Fade out current song over duration (seconds), then stop. */
fadeOut(duration) {
duration = duration || 2;
if (!masterGain || !playing) { this.stopMusic(); return; }
var self = this;
try {
masterGain.gain.linearRampTo(0, duration);
setTimeout(function() { self.stopMusic(); }, duration * 1000 + 100);
} catch (e) { self.stopMusic(); }
},
/** Get the full catalog of songs with metadata. */
getCatalog() {
return getSongs().map(meta);
},
/** Filter songs by criteria: { mood, minEnergy, maxEnergy, minTempo, maxTempo, tags }. */
findSongs(filters) {
if (!filters) return this.getCatalog();
return getSongs().filter(function(s) {
if (filters.mood && s.mood && s.mood.toLowerCase() !== filters.mood.toLowerCase()) return false;
if (filters.minEnergy !== undefined && s.energy < filters.minEnergy) return false;
if (filters.maxEnergy !== undefined && s.energy > filters.maxEnergy) return false;
if (filters.minTempo !== undefined && s.tempo < filters.minTempo) return false;
if (filters.maxTempo !== undefined && s.tempo > filters.maxTempo) return false;
if (filters.tags && filters.tags.length) {
var st = s.tags || [], found = false;
for (var i = 0; i < filters.tags.length; i++)
if (st.indexOf(filters.tags[i]) >= 0) { found = true; break; }
if (!found) return false;
}
return true;
}).map(meta);
},
/** Get metadata for a specific song (includes key and scale). */
getMetadata(songId) {
var result = null;
getSongs().forEach(function(s) {
if (s.id === songId) result = {
id: s.id, name: s.name, mood: s.mood, energy: s.energy, tempo: s.tempo,
key: s.key, scale: s.scale, tags: s.tags, desc: s.desc,
};
});
return result;
},
/** Get currently playing song info, or null. */
getNowPlaying() {
return (currentSong && playing) ? meta(currentSong) : null;
},
/** Check if music is currently playing. */
isPlaying() { return playing; },
/** Pause playback. */
pause() {
if (playing && Tone) try { Tone.getTransport().pause(); } catch (e) {}
},
/** Resume playback after pause. */
resume() {
if (playing && Tone) try { Tone.getTransport().start(); } catch (e) {}
},
};
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+966
View File
@@ -0,0 +1,966 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Avatar Creator - GamerComp</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-dark: #1a1a2e;
--bg-panel: #16213e;
--bg-item: #0f3460;
--accent: #e94560;
--accent-hover: #ff6b6b;
--text: #eee;
--text-muted: #aaa;
--border: #2a3a5e;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-dark);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: var(--bg-panel);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid var(--border);
}
header h1 {
font-size: 1.5rem;
color: var(--accent);
}
.btn {
padding: 0.6rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
transform: scale(1.02);
}
.btn-secondary {
background: var(--bg-item);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--border);
}
main {
flex: 1;
display: flex;
padding: 1rem;
gap: 1rem;
}
/* Preview Panel */
.preview-panel {
width: 300px;
background: var(--bg-panel);
border-radius: 12px;
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
}
.preview-canvas-container {
background: linear-gradient(135deg, #2a3a5e, #1a2a4e);
border-radius: 12px;
padding: 2rem;
margin-bottom: 1.5rem;
}
#previewCanvas {
display: block;
}
.preview-modes {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
justify-content: center;
}
.preview-modes button {
padding: 0.4rem 0.8rem;
font-size: 0.75rem;
border-radius: 6px;
background: var(--bg-item);
border: 1px solid var(--border);
color: var(--text-muted);
cursor: pointer;
}
.preview-modes button.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.preview-animations {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.preview-animations button {
padding: 0.3rem 0.6rem;
font-size: 0.7rem;
border-radius: 4px;
background: var(--bg-dark);
border: 1px solid var(--border);
color: var(--text-muted);
cursor: pointer;
}
.preview-animations button:hover {
border-color: var(--accent);
color: var(--text);
}
/* Customization Panel */
.customization-panel {
flex: 1;
background: var(--bg-panel);
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.category-tabs {
display: flex;
background: var(--bg-dark);
padding: 0.5rem;
gap: 0.5rem;
overflow-x: auto;
}
.category-tab {
padding: 0.6rem 1rem;
border-radius: 8px;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
white-space: nowrap;
font-size: 0.9rem;
transition: all 0.2s;
}
.category-tab:hover {
color: var(--text);
}
.category-tab.active {
background: var(--accent);
color: white;
}
.options-container {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.option-group {
margin-bottom: 1.5rem;
}
.option-group h3 {
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.option-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
gap: 0.5rem;
}
.option-grid.colors {
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
}
.option-item {
aspect-ratio: 1;
border-radius: 8px;
border: 2px solid transparent;
background: var(--bg-item);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
position: relative;
}
.option-item:hover {
border-color: var(--text-muted);
transform: scale(1.05);
}
.option-item.selected {
border-color: var(--accent);
box-shadow: 0 0 10px rgba(233, 69, 96, 0.4);
}
.option-item.color-swatch {
border-radius: 50%;
}
.option-item img, .option-item canvas {
max-width: 80%;
max-height: 80%;
}
.option-item .label {
font-size: 0.6rem;
color: var(--text-muted);
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
}
/* Accessories Panel */
.accessories-panel {
width: 280px;
background: var(--bg-panel);
border-radius: 12px;
padding: 1rem;
display: flex;
flex-direction: column;
}
.accessories-panel h2 {
font-size: 1rem;
margin-bottom: 1rem;
color: var(--accent);
}
.accessory-section {
margin-bottom: 1rem;
}
.accessory-section h3 {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.accessory-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.accessory-item {
aspect-ratio: 1;
border-radius: 6px;
background: var(--bg-item);
border: 2px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.2s;
}
.accessory-item:hover {
border-color: var(--text-muted);
}
.accessory-item.equipped {
border-color: var(--accent);
}
.accessory-item.locked {
opacity: 0.4;
cursor: not-allowed;
}
.accessory-item .level-badge {
position: absolute;
top: 2px;
right: 2px;
font-size: 0.55rem;
background: var(--bg-dark);
padding: 1px 4px;
border-radius: 3px;
color: var(--text-muted);
}
.rarity-common { border-color: #9d9d9d !important; }
.rarity-uncommon { border-color: #1eff00 !important; }
.rarity-rare { border-color: #0070dd !important; }
.rarity-epic { border-color: #a335ee !important; }
.rarity-legendary { border-color: #ff8000 !important; }
.rarity-mythic { border-color: #e6cc80 !important; }
/* Randomize Button */
.randomize-btn {
margin-top: auto;
width: 100%;
padding: 0.8rem;
border-radius: 8px;
background: var(--bg-item);
border: 1px dashed var(--border);
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.randomize-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* Responsive */
@media (max-width: 900px) {
main {
flex-direction: column;
}
.preview-panel, .accessories-panel {
width: 100%;
}
.preview-canvas-container {
padding: 1rem;
}
}
</style>
</head>
<body>
<header>
<h1>Avatar Creator</h1>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-secondary" id="resetBtn">Reset</button>
<button class="btn btn-primary" id="saveBtn">Save Avatar</button>
</div>
</header>
<main>
<!-- Preview Panel -->
<div class="preview-panel">
<div class="preview-canvas-container">
<canvas id="previewCanvas" width="128" height="256"></canvas>
</div>
<div class="preview-modes">
<button data-mode="HEAD_ONLY">Head</button>
<button data-mode="HEAD_AND_SHOULDERS">Shoulders</button>
<button data-mode="UPPER_BODY">Upper</button>
<button data-mode="FULL_BODY" class="active">Full</button>
</div>
<div class="preview-animations">
<button data-anim="idle">Idle</button>
<button data-anim="walk">Walk</button>
<button data-anim="run">Run</button>
<button data-anim="jump">Jump</button>
<button data-anim="wave">Wave</button>
<button data-anim="celebrate">Celebrate</button>
</div>
</div>
<!-- Customization Panel -->
<div class="customization-panel">
<div class="category-tabs">
<button class="category-tab active" data-category="face">Face</button>
<button class="category-tab" data-category="eyes">Eyes</button>
<button class="category-tab" data-category="hair">Hair</button>
<button class="category-tab" data-category="body">Body</button>
<button class="category-tab" data-category="clothing">Clothing</button>
</div>
<div class="options-container" id="optionsContainer">
<!-- Options populated by JavaScript -->
</div>
</div>
<!-- Accessories Panel -->
<div class="accessories-panel">
<h2>Equipped Accessories</h2>
<div class="accessory-section">
<h3>Eyewear</h3>
<div class="accessory-grid" id="eyewearGrid">
<!-- Populated by JavaScript -->
</div>
</div>
<div class="accessory-section">
<h3>Headwear</h3>
<div class="accessory-grid" id="headwearGrid">
<!-- Populated by JavaScript -->
</div>
</div>
<div class="accessory-section">
<h3>Effects</h3>
<div class="accessory-grid" id="effectsGrid">
<!-- Populated by JavaScript -->
</div>
</div>
<button class="randomize-btn" id="randomizeBtn">
<span>🎲</span> Randomize
</button>
</div>
</main>
<!-- Load Avatar System -->
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script>
(function() {
'use strict';
// Current avatar state
var avatar = null;
var currentMode = 'FULL_BODY';
var currentAnimation = 'idle';
var animationFrame = 0;
var lastFrameTime = 0;
// Canvas setup
var canvas = document.getElementById('previewCanvas');
var ctx = canvas.getContext('2d');
// Initialize
function init() {
// Create default avatar
avatar = AvatarSystem.createDefault();
// For demo, give some earned accessories
avatar.earned = {
eyewear: ['sunglasses-classic', 'nerd-glasses', 'aviator'],
headwear: ['baseball-cap', 'beanie', 'crown-gold'],
effects: ['sparkle-trail']
};
avatar.level = 42;
// Setup UI
setupCategoryTabs();
setupModeButtons();
setupAnimationButtons();
setupAccessoriesPanel();
renderOptions('face');
// Start render loop
requestAnimationFrame(renderLoop);
}
// Render loop
function renderLoop(time) {
// Update animation frame
var anim = window.AvatarAnimations && AvatarAnimations.getAnimation(currentMode, currentAnimation);
if (anim) {
var frameDuration = anim.frameDuration || 100;
if (time - lastFrameTime > frameDuration) {
animationFrame = (animationFrame + 1) % anim.frameCount;
lastFrameTime = time;
}
}
// Clear and render
ctx.clearRect(0, 0, canvas.width, canvas.height);
var modeSize = window.AvatarSystem ? AvatarSystem.getModeSize(currentMode) : { width: 64, height: 160 };
var scale = Math.min(
(canvas.width - 20) / modeSize.width,
(canvas.height - 20) / modeSize.height
);
var x = canvas.width / 2;
var y = canvas.height / 2 + (modeSize.height * scale) / 4;
if (window.AvatarSystem) {
AvatarSystem.render(avatar, currentMode, ctx, x, y, {
scale: scale,
animation: currentAnimation,
frame: animationFrame,
time: time / 1000
});
} else {
// Fallback placeholder
ctx.fillStyle = '#e94560';
ctx.beginPath();
ctx.arc(x, y - 40, 30, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#16213e';
ctx.fillRect(x - 20, y - 5, 40, 50);
}
requestAnimationFrame(renderLoop);
}
// Category tabs
function setupCategoryTabs() {
var tabs = document.querySelectorAll('.category-tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function() {
tabs.forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
renderOptions(tab.dataset.category);
});
});
}
// Mode buttons
function setupModeButtons() {
var buttons = document.querySelectorAll('.preview-modes button');
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
buttons.forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
currentMode = btn.dataset.mode;
animationFrame = 0;
});
});
}
// Animation buttons
function setupAnimationButtons() {
var buttons = document.querySelectorAll('.preview-animations button');
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
currentAnimation = btn.dataset.anim;
animationFrame = 0;
});
});
}
// Render customization options
function renderOptions(category) {
var container = document.getElementById('optionsContainer');
container.innerHTML = '';
if (!window.AvatarData) {
container.innerHTML = '<p style="color: var(--text-muted); padding: 2rem;">Loading options...</p>';
return;
}
switch (category) {
case 'face':
renderFaceOptions(container);
break;
case 'eyes':
renderEyeOptions(container);
break;
case 'hair':
renderHairOptions(container);
break;
case 'body':
renderBodyOptions(container);
break;
case 'clothing':
renderClothingOptions(container);
break;
}
}
function renderFaceOptions(container) {
// Face Shape
var group1 = createOptionGroup('Face Shape');
var grid1 = createOptionGrid();
AvatarData.FACE_SHAPES.forEach(function(shape) {
var item = createOptionItem(shape.name, shape.id === avatar.base.faceShape);
item.addEventListener('click', function() {
avatar.base.faceShape = shape.id;
renderOptions('face');
});
grid1.appendChild(item);
});
group1.appendChild(grid1);
container.appendChild(group1);
// Skin Tone
var group2 = createOptionGroup('Skin Tone');
var grid2 = createOptionGrid('colors');
AvatarData.SKIN_TONES.forEach(function(tone) {
var item = createColorSwatch(tone.hex, avatar.base.skinTone === tone.hex);
item.addEventListener('click', function() {
avatar.base.skinTone = tone.hex;
renderOptions('face');
});
grid2.appendChild(item);
});
group2.appendChild(grid2);
container.appendChild(group2);
// Nose
var group3 = createOptionGroup('Nose');
var grid3 = createOptionGrid();
AvatarData.NOSE_SHAPES.forEach(function(nose) {
var item = createOptionItem(nose.name, nose.id === avatar.base.nose);
item.addEventListener('click', function() {
avatar.base.nose = nose.id;
renderOptions('face');
});
grid3.appendChild(item);
});
group3.appendChild(grid3);
container.appendChild(group3);
// Mouth
var group4 = createOptionGroup('Mouth');
var grid4 = createOptionGrid();
AvatarData.MOUTH_SHAPES.forEach(function(mouth) {
var item = createOptionItem(mouth.name, mouth.id === avatar.base.mouth);
item.addEventListener('click', function() {
avatar.base.mouth = mouth.id;
renderOptions('face');
});
grid4.appendChild(item);
});
group4.appendChild(grid4);
container.appendChild(group4);
}
function renderEyeOptions(container) {
// Eye Shape
var group1 = createOptionGroup('Eye Shape');
var grid1 = createOptionGrid();
AvatarData.EYE_SHAPES.forEach(function(shape) {
var item = createOptionItem(shape.name, shape.id === avatar.base.eyes.shape);
item.addEventListener('click', function() {
avatar.base.eyes.shape = shape.id;
renderOptions('eyes');
});
grid1.appendChild(item);
});
group1.appendChild(grid1);
container.appendChild(group1);
// Eye Color
var group2 = createOptionGroup('Eye Color');
var grid2 = createOptionGrid('colors');
AvatarData.EYE_COLORS.forEach(function(color) {
var item = createColorSwatch(color.hex, color.id === avatar.base.eyes.color);
item.addEventListener('click', function() {
avatar.base.eyes.color = color.id;
renderOptions('eyes');
});
grid2.appendChild(item);
});
group2.appendChild(grid2);
container.appendChild(group2);
// Eyebrows
var group3 = createOptionGroup('Eyebrows');
var grid3 = createOptionGrid();
AvatarData.EYEBROW_SHAPES.forEach(function(brow) {
var item = createOptionItem(brow.name, brow.id === avatar.base.eyebrows);
item.addEventListener('click', function() {
avatar.base.eyebrows = brow.id;
renderOptions('eyes');
});
grid3.appendChild(item);
});
group3.appendChild(grid3);
container.appendChild(group3);
}
function renderHairOptions(container) {
// Hair Style
var group1 = createOptionGroup('Hair Style');
var grid1 = createOptionGrid();
AvatarData.HAIR_STYLES.forEach(function(style) {
var item = createOptionItem(style.name, style.id === avatar.base.hair.style);
item.addEventListener('click', function() {
avatar.base.hair.style = style.id;
renderOptions('hair');
});
grid1.appendChild(item);
});
group1.appendChild(grid1);
container.appendChild(group1);
// Hair Color
var group2 = createOptionGroup('Hair Color');
var grid2 = createOptionGrid('colors');
AvatarData.HAIR_COLORS.forEach(function(color) {
var item = createColorSwatch(color.hex, avatar.base.hair.color === color.hex);
item.addEventListener('click', function() {
avatar.base.hair.color = color.hex;
renderOptions('hair');
});
grid2.appendChild(item);
});
group2.appendChild(grid2);
container.appendChild(group2);
// Facial Hair
var group3 = createOptionGroup('Facial Hair');
var grid3 = createOptionGrid();
AvatarData.FACIAL_HAIR.forEach(function(fh) {
var item = createOptionItem(fh.name, fh.id === avatar.base.facialHair);
item.addEventListener('click', function() {
avatar.base.facialHair = fh.id;
renderOptions('hair');
});
grid3.appendChild(item);
});
group3.appendChild(grid3);
container.appendChild(group3);
}
function renderBodyOptions(container) {
// Body Type
var group1 = createOptionGroup('Body Type');
var grid1 = createOptionGrid();
AvatarData.BODY_TYPES.forEach(function(type) {
var item = createOptionItem(type.name, type.id === avatar.base.body.type);
item.addEventListener('click', function() {
avatar.base.body.type = type.id;
renderOptions('body');
});
grid1.appendChild(item);
});
group1.appendChild(grid1);
container.appendChild(group1);
}
function renderClothingOptions(container) {
// Clothing Style
var group1 = createOptionGroup('Style');
var grid1 = createOptionGrid();
AvatarData.CLOTHING_STYLES.forEach(function(style) {
var item = createOptionItem(style.name, style.id === avatar.base.clothing.style);
item.addEventListener('click', function() {
avatar.base.clothing.style = style.id;
renderOptions('clothing');
});
grid1.appendChild(item);
});
group1.appendChild(grid1);
container.appendChild(group1);
// Shirt Color
var group2 = createOptionGroup('Shirt Color');
var grid2 = createOptionGrid('colors');
AvatarData.CLOTHING_COLORS.forEach(function(color) {
var item = createColorSwatch(color, avatar.base.clothing.shirtColor === color);
item.addEventListener('click', function() {
avatar.base.clothing.shirtColor = color;
renderOptions('clothing');
});
grid2.appendChild(item);
});
group2.appendChild(grid2);
container.appendChild(group2);
// Pants Color
var group3 = createOptionGroup('Pants Color');
var grid3 = createOptionGrid('colors');
AvatarData.CLOTHING_COLORS.forEach(function(color) {
var item = createColorSwatch(color, avatar.base.clothing.pantsColor === color);
item.addEventListener('click', function() {
avatar.base.clothing.pantsColor = color;
renderOptions('clothing');
});
grid3.appendChild(item);
});
group3.appendChild(grid3);
container.appendChild(group3);
}
// Accessories panel
function setupAccessoriesPanel() {
updateAccessoriesUI();
document.getElementById('randomizeBtn').addEventListener('click', randomizeAvatar);
document.getElementById('resetBtn').addEventListener('click', function() {
avatar = AvatarSystem.createDefault();
avatar.earned = { eyewear: ['sunglasses-classic'], headwear: ['baseball-cap'], effects: [] };
avatar.level = 1;
renderOptions('face');
updateAccessoriesUI();
});
document.getElementById('saveBtn').addEventListener('click', function() {
// Would save to API
console.log('Saving avatar:', JSON.stringify(avatar, null, 2));
alert('Avatar saved! (Check console for data)');
});
}
function updateAccessoriesUI() {
if (!window.AvatarAccessories) return;
// Eyewear
var eyewearGrid = document.getElementById('eyewearGrid');
eyewearGrid.innerHTML = '';
var eyewearItems = AvatarAccessories.getByCategory('eyewear').slice(0, 8);
eyewearItems.forEach(function(item) {
var el = createAccessoryItem(item, 'eyewear');
eyewearGrid.appendChild(el);
});
// Headwear
var headwearGrid = document.getElementById('headwearGrid');
headwearGrid.innerHTML = '';
var headwearItems = AvatarAccessories.getByCategory('headwear').slice(0, 8);
headwearItems.forEach(function(item) {
var el = createAccessoryItem(item, 'headwear');
headwearGrid.appendChild(el);
});
// Effects
var effectsGrid = document.getElementById('effectsGrid');
effectsGrid.innerHTML = '';
var effectItems = AvatarAccessories.getByCategory('effect').slice(0, 8);
effectItems.forEach(function(item) {
var el = createAccessoryItem(item, 'effect');
effectsGrid.appendChild(el);
});
}
function createAccessoryItem(item, category) {
var div = document.createElement('div');
div.className = 'accessory-item';
var earned = avatar.earned[category === 'effect' ? 'effects' : category] || [];
var isEarned = earned.includes(item.id);
var isEquipped = avatar.equipped[category] === item.id;
if (!isEarned) {
div.classList.add('locked');
div.innerHTML = '<span class="level-badge">Lv' + item.unlockLevel + '</span>';
} else {
div.classList.add('rarity-' + item.rarity);
if (isEquipped) div.classList.add('equipped');
div.innerHTML = '<span style="font-size:1.2rem">' + getAccessoryEmoji(item.id) + '</span>';
div.addEventListener('click', function() {
if (isEquipped) {
avatar.equipped[category] = null;
} else {
avatar.equipped[category] = item.id;
}
updateAccessoriesUI();
});
}
div.title = item.name;
return div;
}
function getAccessoryEmoji(id) {
var emojis = {
'sunglasses-classic': '🕶️', 'aviator': '🥽', 'nerd-glasses': '🤓',
'baseball-cap': '🧢', 'beanie': '🎿', 'crown-gold': '👑',
'sparkle-trail': '✨', 'fire-aura': '🔥', 'lightning-crackle': '⚡'
};
return emojis[id] || '🎁';
}
// Helper functions
function createOptionGroup(title) {
var div = document.createElement('div');
div.className = 'option-group';
var h3 = document.createElement('h3');
h3.textContent = title;
div.appendChild(h3);
return div;
}
function createOptionGrid(type) {
var div = document.createElement('div');
div.className = 'option-grid' + (type ? ' ' + type : '');
return div;
}
function createOptionItem(label, selected) {
var div = document.createElement('div');
div.className = 'option-item' + (selected ? ' selected' : '');
var span = document.createElement('span');
span.className = 'label';
span.textContent = label.length > 8 ? label.substring(0, 7) + '…' : label;
div.appendChild(span);
return div;
}
function createColorSwatch(color, selected) {
var div = document.createElement('div');
div.className = 'option-item color-swatch' + (selected ? ' selected' : '');
div.style.backgroundColor = color;
return div;
}
function randomizeAvatar() {
if (!window.AvatarData) return;
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
avatar.base.faceShape = pick(AvatarData.FACE_SHAPES).id;
avatar.base.skinTone = pick(AvatarData.SKIN_TONES).hex;
avatar.base.eyes.shape = pick(AvatarData.EYE_SHAPES).id;
avatar.base.eyes.color = pick(AvatarData.EYE_COLORS).id;
avatar.base.eyebrows = pick(AvatarData.EYEBROW_SHAPES).id;
avatar.base.nose = pick(AvatarData.NOSE_SHAPES).id;
avatar.base.mouth = pick(AvatarData.MOUTH_SHAPES).id;
avatar.base.hair.style = pick(AvatarData.HAIR_STYLES).id;
avatar.base.hair.color = pick(AvatarData.HAIR_COLORS).hex;
avatar.base.facialHair = pick(AvatarData.FACIAL_HAIR).id;
avatar.base.body.type = pick(AvatarData.BODY_TYPES).id;
avatar.base.clothing.style = pick(AvatarData.CLOTHING_STYLES).id;
avatar.base.clothing.shirtColor = pick(AvatarData.CLOTHING_COLORS);
avatar.base.clothing.pantsColor = pick(AvatarData.CLOTHING_COLORS);
renderOptions(document.querySelector('.category-tab.active').dataset.category);
}
// Initialize when ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>
+210
View File
@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clear Cache - GamerComp</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
color: white;
min-height: 100vh;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: rgba(255,255,255,0.1);
border-radius: 16px;
padding: 32px;
max-width: 450px;
text-align: center;
}
h1 { margin: 0 0 16px; font-size: 24px; }
p { margin: 0 0 20px; opacity: 0.8; line-height: 1.5; }
button {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
border: none;
padding: 16px 32px;
border-radius: 12px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
width: 100%;
margin-bottom: 12px;
transition: opacity 0.2s;
}
button:hover { opacity: 0.9; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-secondary {
background: rgba(255,255,255,0.2);
}
.status {
margin-top: 20px;
padding: 16px;
border-radius: 12px;
background: rgba(255,255,255,0.1);
text-align: left;
font-size: 14px;
line-height: 1.6;
}
.success { background: rgba(34, 197, 94, 0.3); }
.error { background: rgba(239, 68, 68, 0.3); }
a { color: #a5b4fc; }
.divider {
height: 1px;
background: rgba(255,255,255,0.2);
margin: 24px 0;
}
.manual-section {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 16px;
margin-top: 20px;
}
.manual-section h3 {
margin: 0 0 12px;
font-size: 16px;
}
.shortcut {
display: inline-block;
background: rgba(0,0,0,0.3);
padding: 4px 10px;
border-radius: 6px;
font-family: monospace;
font-size: 14px;
margin: 2px;
}
.steps {
text-align: left;
font-size: 14px;
opacity: 0.9;
}
.steps li {
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔄 Clear Cache</h1>
<p>Having issues seeing updates? Clear all cached data for GamerComp.</p>
<button onclick="clearAllCaches()" id="clearBtn">Clear All Caches</button>
<button onclick="forceReload()" class="btn-secondary" id="reloadBtn">Force Hard Reload</button>
<div id="status" class="status" style="display:none;"></div>
<div class="manual-section">
<h3>🔧 Manual Hard Refresh</h3>
<p style="font-size: 13px; margin-bottom: 12px;">If the buttons above don't work, try these keyboard shortcuts:</p>
<p style="margin-bottom: 8px;">
<strong>Windows/Linux:</strong><br>
<span class="shortcut">Ctrl + Shift + R</span> or <span class="shortcut">Ctrl + F5</span>
</p>
<p style="margin-bottom: 0;">
<strong>Mac:</strong><br>
<span class="shortcut">Cmd + Shift + R</span>
</p>
</div>
<div class="divider"></div>
<p style="font-size: 14px; margin-bottom: 0;">
<a href="/">← Back to GamerComp</a>
</p>
</div>
<script>
async function clearAllCaches() {
const btn = document.getElementById('clearBtn');
const status = document.getElementById('status');
btn.disabled = true;
btn.textContent = 'Clearing...';
status.style.display = 'block';
status.className = 'status';
status.textContent = 'Working...';
let results = [];
try {
// 1. Unregister all service workers
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
await registration.unregister();
results.push('✓ Service worker unregistered');
}
if (registrations.length === 0) {
results.push('✓ No service workers to remove');
}
}
// 2. Clear all caches (Cache API)
if ('caches' in window) {
const cacheNames = await caches.keys();
for (const cacheName of cacheNames) {
await caches.delete(cacheName);
results.push(`✓ Cache "${cacheName}" deleted`);
}
if (cacheNames.length === 0) {
results.push('✓ No caches to clear');
}
}
// 3. Clear localStorage (except auth token)
const token = localStorage.getItem('token');
localStorage.clear();
if (token) {
localStorage.setItem('token', token);
results.push('✓ LocalStorage cleared (kept login)');
} else {
results.push('✓ LocalStorage cleared');
}
// 4. Clear sessionStorage
sessionStorage.clear();
results.push('✓ SessionStorage cleared');
status.className = 'status success';
status.innerHTML = `
<strong>✅ All caches cleared!</strong><br><br>
${results.join('<br>')}<br><br>
<strong>Next step:</strong> Click "Force Hard Reload" below or press <span class="shortcut">Ctrl+Shift+R</span> to reload with fresh files.
`;
btn.textContent = 'Done!';
} catch (err) {
status.className = 'status error';
status.textContent = 'Error: ' + err.message;
btn.disabled = false;
btn.textContent = 'Try Again';
}
}
function forceReload() {
const status = document.getElementById('status');
status.style.display = 'block';
status.className = 'status';
status.textContent = 'Reloading with fresh files...';
// Add cache-busting query param and reload
// Using location.href with a timestamp forces a fresh fetch
const freshUrl = '/?_=' + Date.now();
// Try to clear browser cache via fetch with cache: 'reload'
// Then navigate to homepage
fetch('/', { cache: 'reload' })
.finally(() => {
window.location.href = freshUrl;
});
}
</script>
</body>
</html>
+266
View File
@@ -0,0 +1,266 @@
# Adding New Assets
Guide for extending the GamerComp asset library.
## Adding New Music
### 1. Define Song in musicLibrary.js
```javascript
{
id: 'epic-21',
name: 'Dawn of Heroes',
mood: 'Epic',
energy: 8,
tempo: 130,
key: 'G',
scale: 'minor',
tags: ['battle', 'adventure', 'cinematic'],
desc: 'Triumphant orchestral piece for heroic moments',
// Instrument configuration
synths: {
melody: { type: 'AMSynth', options: { harmonicity: 2 } },
bass: { type: 'FMSynth', options: { modulationIndex: 10 } },
pad: { type: 'PolySynth', options: { voices: 4 } }
},
// Effects chain
fx: {
reverb: { wet: 0.4, decay: 2 },
delay: { wet: 0.2, time: '8n' }
},
// Song sections with chord progressions
sections: {
intro: {
chords: ['i', 'VI', 'III', 'VII'],
duration: 8,
instruments: ['pad']
},
verse: {
chords: ['i', 'iv', 'VI', 'V'],
duration: 16,
instruments: ['melody', 'bass', 'pad']
},
chorus: {
chords: ['VI', 'VII', 'i', 'i'],
duration: 16,
instruments: ['melody', 'bass', 'pad', 'drums']
}
},
// Section order
form: ['intro', 'verse', 'chorus', 'verse', 'chorus', 'chorus']
}
```
### 2. Update Catalog Stats
In `musicLibrary.js`, increment the count.
### 3. Test
```javascript
MusicEngine.playSong('epic-21');
```
---
## Adding New Background Theme
### 1. Create Theme File
Create `themes/newtheme.js`:
```javascript
(function(engine) {
if (!engine) return;
engine.registerTheme('newtheme', {
name: 'New Theme',
description: 'Description of theme',
variants: {
variant1: {
meta: {
name: 'Variant One',
mood: 'wonder',
tags: ['tag1', 'tag2'],
description: 'Description',
compatibleMusicMoods: ['Epic', 'Heroic'],
recommendedFor: ['action', 'adventure']
},
colors: ['#000000', '#111111', '#222222', '#333333'],
render: function(ctx, w, h, time, colors, opts) {
// Background layer
var gradient = ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, colors[0]);
gradient.addColorStop(1, colors[1]);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
// Animated elements
// Use 'time' for animation (seconds)
}
},
// Add 7 more variants for 8 total
}
});
})(window.BackgroundEngine);
```
### 2. Add Script Include
In templates, add:
```html
<script src="/assets/backgrounds/themes/newtheme.js"></script>
```
### 3. Update Catalog
Add to `THEME_VARIANTS` in `backgroundCatalog.js`.
### 4. Test
```javascript
BackgroundEngine.render(ctx, 'newtheme', 'variant1', w, h, 0);
```
---
## Adding New Avatar Accessory
### 1. Add to accessories.js
```javascript
// In EYEWEAR, HEADWEAR, or EFFECTS array:
{
id: 'new-item',
name: 'New Item',
category: 'headwear',
unlockLevel: 50,
rarity: 'epic',
zIndex: 10,
anchor: 'top',
offset: { x: 0, y: -15 },
scale: 1.0,
hasEffect: false,
description: 'Description of item'
}
```
### 2. Add Renderer in accessoryRenderer.js
```javascript
// In appropriate RENDERERS object:
'new-item': function(ctx, x, y, scale, color, time) {
// Draw the accessory
ctx.fillStyle = color || '#gold';
ctx.beginPath();
// ... drawing code
ctx.fill();
}
```
### 3. Test
```javascript
AvatarSystem.equipAccessory(avatar, 'headwear', 'new-item');
```
---
## Adding New Avatar Context
### 1. Add to Appropriate File
For vehicles, add to `contexts/vehicles.js`:
```javascript
{
id: 'new-vehicle',
name: 'New Vehicle',
category: 'vehicle',
avatarMode: 'HEAD_AND_SHOULDERS',
animations: ['idle', 'hurt', 'celebrate'],
defaultAnimation: 'idle',
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
layerOrder: ['background', 'avatar', 'foreground'],
recommendedFor: ['action', 'racing'],
description: 'Description',
avatarPosition: { x: 0.5, y: 0.4 },
avatarScale: 1.0,
renderBackground: function(ctx, w, h, time, state) {
// Draw background
},
renderForeground: function(ctx, w, h, time, state) {
// Draw foreground overlay
},
contextEffects: {
hurt: { type: 'shake', intensity: 5 }
}
}
```
### 2. Update Catalog Mappings
In `contextCatalog.js`:
- Add to `GAME_TYPE_CONTEXTS`
- Will auto-aggregate via `getAll()`
### 3. Test
```javascript
const awc = ContextRenderer.apply(avatar, 'new-vehicle');
ContextRenderer.render(awc, ctx, x, y);
```
---
## Adding New Preset
### 1. Add to presets.js
```javascript
{
id: 'style-theme-##',
gameStyle: 'action-arcade',
name: 'Preset Name',
description: 'What this combination provides',
music: { mood: 'Epic', energy: 8 },
background: { theme: 'space', variant: 'nebula' },
avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' },
colorPalette: 'space-blue',
tags: ['tag1', 'tag2']
}
```
### 2. Validate
```javascript
const result = AssetCompatibility.validate(newPreset);
console.log(result.score); // Should be > 70
```
---
## Testing Guidelines
1. **Visual Testing**: Use asset-browser.html to preview
2. **Compatibility**: Run `AssetCompatibility.validate()`
3. **Performance**: Test animation at 60fps
4. **Mobile**: Test touch interactions
5. **Integration**: Test in actual game template
## File Size Guidelines
- Keep individual JS files under 100KB
- Music library can be larger (songs are data)
- Optimize render functions (no complex math in loops)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,598 @@
# GamerComp Asset Library
## Overview
The GamerComp Asset Library is a comprehensive procedural asset system providing:
- **160 Procedural Songs** - 8 moods x 20 songs each using Tone.js
- **120 Procedural Backgrounds** - 15 themes x 8 variants using Canvas 2D
- **Full Avatar System** - Mii-style customizable avatars with 45 accessories
- **26 Game Contexts** - Place avatars in vehicles, costumes, or pure modes
- **220 Presets** - Curated asset combinations for 11 game styles
All assets are procedurally generated - no external images or audio files needed.
---
## System Architecture
```
+--------------------------------------------------------------------------------+
| LAYER 4: ASSET MANAGER |
| |
| +----------------+ +----------------+ +-----------+ +----------------+ |
| | assetManager.js| |assetCatalog.js | | presets.js| |compatibility.js| |
| | (coordination) | |(unified query) | | (220) | | (validation) | |
| +-------+--------+ +-------+--------+ +-----+-----+ +--------+-------+ |
| | | | | |
+-----------+-------------------+-----------------+-----------------+------------+
| | | |
v v v v
+--------------------------------------------------------------------------------+
| LAYER 3: CONTEXT SYSTEM |
| |
| +-------------+ +-------------+ +-------------+ +------------------+ |
| | vehicles.js | | costumes.js | | pure.js | | contextRenderer | |
| | (12) | | (10) | | (4) | | .js | |
| +-------------+ +-------------+ +-------------+ +------------------+ |
| |
+--------------------------------------------------------------------------------+
| | |
v v v
+--------------------------------------------------------------------------------+
| LAYER 2: CONTENT LIBRARIES |
| |
| +------------------+ +------------------------+ +-----------------+ |
| | musicLibrary.js | | 15 Background Theme | | avatarData.js | |
| | (160 songs) | | Files (8 variants ea) | | accessories.js | |
| +------------------+ +------------------------+ | (45 items) | |
| | space.js nature.js | +-----------------+ |
| | urban.js fantasy.js | |
| | abstract.js retro.js | |
| | indoor.js weather.js | |
| | sports.js seasonal.js | |
| | underwater.js sky.js | |
| | mechanical.js spooky.js| |
| | colorful.js | |
| +------------------------+ |
| |
+--------------------------------------------------------------------------------+
| | |
v v v
+--------------------------------------------------------------------------------+
| LAYER 1: CORE ENGINES |
| |
| +------------------+ +--------------------+ +-------------------+ |
| | MusicEngine | | BackgroundEngine | | AvatarSystem | |
| | (Tone.js) | | (Canvas 2D) | | (Canvas 2D) | |
| +------------------+ +--------------------+ +-------------------+ |
| |
+--------------------------------------------------------------------------------+
```
---
## Quick Start
### 1. Include Scripts
Scripts must be loaded in dependency order. Here is the recommended `<script>` order:
```html
<!-- External dependency -->
<script src="https://unpkg.com/tone@14.7.77/build/Tone.js"></script>
<!-- Layer 1: Core Engines -->
<script src="/assets/music/musicEngine.js"></script>
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<!-- Layer 2: Content Libraries -->
<script src="/assets/music/musicLibrary.js"></script>
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/backgrounds/themes/space.js"></script>
<script src="/assets/backgrounds/themes/nature.js"></script>
<script src="/assets/backgrounds/themes/urban.js"></script>
<script src="/assets/backgrounds/themes/fantasy.js"></script>
<script src="/assets/backgrounds/themes/abstract.js"></script>
<script src="/assets/backgrounds/themes/retro.js"></script>
<script src="/assets/backgrounds/themes/indoor.js"></script>
<script src="/assets/backgrounds/themes/weather.js"></script>
<script src="/assets/backgrounds/themes/sports.js"></script>
<script src="/assets/backgrounds/themes/seasonal.js"></script>
<script src="/assets/backgrounds/themes/underwater.js"></script>
<script src="/assets/backgrounds/themes/sky.js"></script>
<script src="/assets/backgrounds/themes/mechanical.js"></script>
<script src="/assets/backgrounds/themes/spooky.js"></script>
<script src="/assets/backgrounds/themes/colorful.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<!-- Layer 3: Context System -->
<script src="/assets/avatar/contexts/vehicles.js"></script>
<script src="/assets/avatar/contexts/costumes.js"></script>
<script src="/assets/avatar/contexts/pure.js"></script>
<script src="/assets/avatar/contextCatalog.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<!-- Layer 4: Asset Manager -->
<script src="/assets/assetCatalog.js"></script>
<script src="/assets/compatibility.js"></script>
<script src="/assets/presets.js"></script>
<script src="/assets/assetManager.js"></script>
```
### 2. Initialize
```javascript
AssetManager.initialize().then(function(stats) {
console.log('Asset Library initialized:', stats);
// {
// music: { songs: 160 },
// backgrounds: { count: 120 },
// avatarContexts: { count: 26 },
// presets: { count: 220 }
// }
});
```
### 3. Get Recommendations
```javascript
// Get AI-powered asset recommendations based on game description
var recommendations = AssetManager.getRecommendations(
'action-arcade', // Game style
'space shooter with aliens', // Description/keywords
'Epic' // Optional mood override
);
// Returns top 3 recommendations with explanations
// [
// {
// config: { music: {...}, background: {...}, avatarContext: {...} },
// score: 85,
// explanation: 'Epic high-energy music sets the mood; space nebula background...',
// detectedTheme: 'space',
// detectedMood: 'epic',
// detectedMechanics: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
// },
// ...
// ]
```
### 4. Load Assets
```javascript
await AssetManager.loadGameAssets({
music: { mood: 'Epic', energy: 8 },
background: { theme: 'space', variant: 'nebula' },
avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
});
// Access loaded assets
var assets = AssetManager.getLoadedAssets();
// {
// music: { id: 'epic_001', name: 'Siege of Thunder', ... },
// background: { theme: 'space', variant: 'nebula', render: function(...) },
// avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
// }
```
---
## Benefits
| Benefit | Description |
|---------|-------------|
| **Zero External Dependencies** | Everything procedurally generated - no images, audio files, or CDN requirements |
| **Consistent Style** | All assets designed to work together with unified color palettes and visual language |
| **Intelligent Matching** | Compatibility system ensures good combinations with mood/theme/context scoring |
| **Variety** | 220 presets provide instant diversity for any game style |
| **Customizable** | Override any setting while keeping coherence through validation feedback |
| **Lightweight** | All code compresses to ~150KB total (before gzip) |
| **Offline-First** | Works without network after initial load |
---
## Asset Counts Summary
```
+---------------------+--------+-----------------------------------------+
| Category | Count | Details |
+---------------------+--------+-----------------------------------------+
| Music Songs | 160 | 8 moods x 20 songs each |
| Background Variants | 120 | 15 themes x 8 variants each |
| Avatar Accessories | 45 | Eyewear, headwear, effects, particles |
| Game Contexts | 26 | 12 vehicles + 10 costumes + 4 pure |
| Presets | 220 | 20 per game style x 11 game styles |
| Game Styles | 11 | Action, Puzzle, Story, Racing, etc. |
| Music Moods | 8 | Epic, Chill, Intense, Playful, etc. |
| Background Themes | 15 | Space, Nature, Urban, Fantasy, etc. |
| Color Palettes | 11 | Pre-defined harmonious color sets |
+---------------------+--------+-----------------------------------------+
```
---
## Music System
### Moods (8 total, 20 songs each)
| Mood | Energy Range | Description | Tags |
|------|--------------|-------------|------|
| **Epic** | 7-10 | Orchestral, triumphant, powerful | battle, hero, conquest |
| **Chill** | 1-4 | Relaxed, ambient, peaceful | calm, zen, meditation |
| **Intense** | 7-10 | Fast, aggressive, urgent | action, chase, combat |
| **Playful** | 4-7 | Fun, bouncy, whimsical | happy, silly, cartoon |
| **Mysterious** | 3-6 | Suspenseful, eerie, enigmatic | dark, secret, shadow |
| **Heroic** | 6-9 | Triumphant, brave, noble | victory, champion, glory |
| **Quirky** | 4-7 | Unusual, offbeat, eccentric | weird, wacky, unique |
| **Ambient** | 1-4 | Atmospheric, floating, ethereal | space, dreamy, soft |
### Song Structure
Each song is defined with:
- **id**: Unique identifier (e.g., `epic_001`)
- **name**: Human-readable name
- **mood**: One of the 8 mood categories
- **energy**: 1-10 scale
- **tempo**: BPM (60-180)
- **key**: Musical key (C, D, E, etc.)
- **scale**: Scale type (minor, major, dorian, phrygian, etc.)
- **synths**: Lead, pad, and bass instrument types
- **fx**: Reverb, delay, filter settings
- **sections**: Intro, verse, chorus, bridge, outro with chord progressions
- **form**: Section playback order
---
## Background System
### Themes (15 total, 8 variants each)
| Theme | Variants | Mood | Recommended For |
|-------|----------|------|-----------------|
| **space** | nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole | wonder, epic | Shooters, Sci-Fi |
| **nature** | forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow | serene, peaceful | Adventure, Platformers |
| **urban** | citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley | energetic, gritty | Racing, Action |
| **fantasy** | castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower | magical, wonder | RPG, Adventure |
| **abstract** | geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix | focused, trippy | Puzzle, Creative |
| **retro** | pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave | nostalgic, fun | Arcade, Classic |
| **indoor** | laboratory, classroom, bedroom, kitchen, arcade, office, library, gym | cozy, focused | Puzzle, Educational |
| **weather** | rain, snow, storm, sunset, sunrise, fog, lightning, aurora | atmospheric, dynamic | Adventure, Mood |
| **sports** | stadium, raceTrack, court, field, arena, pool, gym, skatepark | energetic, competitive | Sports, Racing |
| **seasonal** | springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical | nostalgic, themed | Casual, Seasonal |
| **underwater** | coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure | mysterious, serene | Adventure, Exploration |
| **sky** | clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield | dreamy, free | Flying, Casual |
| **mechanical** | gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab | industrial, complex | Puzzle, Tech |
| **spooky** | hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows | eerie, tense | Horror-lite, Halloween |
| **colorful** | rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism | vibrant, joyful | Kids, Casual |
---
## Avatar Context System
### Context Categories
#### Vehicles (12 contexts)
Place your avatar inside vehicles with appropriate framing:
| Context | Avatar Mode | Description | Recommended For |
|---------|-------------|-------------|-----------------|
| spaceship-cockpit | HEAD_ONLY | Pilot view in spacecraft | Space shooters |
| race-car | HEAD_AND_SHOULDERS | Racing car driver | Racing games |
| airplane | HEAD_ONLY | Airplane pilot | Flying games |
| flappy-style | HEAD_ONLY | Bird/flying creature | Flappy-type games |
| tank | HEAD_ONLY | Tank commander | Action games |
| pacman-style | FULL_BODY | Classic maze runner | Arcade games |
| hoverboard | FULL_BODY | Futuristic board rider | Racing, Action |
| jetpack | FULL_BODY | Jetpack flyer | Flying, Action |
| mech-suit | HEAD_ONLY | Mech pilot | Action, Sci-Fi |
| submarine | HEAD_AND_SHOULDERS | Sub commander | Underwater |
| dragon-rider | FULL_BODY | Dragon mount | Fantasy, Adventure |
| motorcycle | FULL_BODY | Motorcycle rider | Racing |
#### Costumes (10 contexts)
Dress your avatar in themed outfits:
| Context | Avatar Mode | Description | Recommended For |
|---------|-------------|-------------|-----------------|
| knight-armor | FULL_BODY | Medieval knight | RPG, Adventure |
| space-suit | FULL_BODY | Astronaut suit | Space, Sci-Fi |
| ninja-outfit | FULL_BODY | Stealth ninja | Action, Stealth |
| wizard-robes | FULL_BODY | Magic user | Fantasy, RPG |
| superhero-cape | FULL_BODY | Superhero costume | Action |
| athlete-uniform | FULL_BODY | Sports gear | Sports |
| pirate-outfit | FULL_BODY | Pirate costume | Adventure |
| scientist-labcoat | FULL_BODY | Lab scientist | Puzzle, Educational |
| chef-outfit | FULL_BODY | Chef uniform | Cooking, Casual |
| cowboy-gear | FULL_BODY | Western attire | Adventure |
#### Pure/Standard (4 contexts)
Minimal modifications for classic gameplay:
| Context | Avatar Mode | Description | Recommended For |
|---------|-------------|-------------|-----------------|
| platformer-standard | FULL_BODY | Classic platformer | Platformers |
| sports-player | FULL_BODY | Generic athlete | Sports |
| adventure-hero | FULL_BODY | Generic adventurer | Adventure |
| runner | FULL_BODY | Running character | Endless runners |
### Avatar Display Modes
```
+----------------+ +-----------------------+ +-------------------+
| HEAD_ONLY | | HEAD_AND_SHOULDERS | | FULL_BODY |
| | | | | |
| .---. | | .---. | | .---. |
| ( o o ) | | ( o o ) | | ( o o ) |
| '-.-' | | '-.-' | | '-.-' |
| | | /| |\ | | /| |\ |
| [Vehicle | | / | | \ | | / | | \ |
| Cockpit] | | [Upper Body] | | / \ |
| | | | | / \ |
+----------------+ +-----------------------+ +-------------------+
Used for: Used for: Used for:
- Cockpits - Racing vehicles - Platformers
- First-person - Seated contexts - Full character
- Tight frames - Partial views - Action games
```
---
## Preset System
### Game Styles (11 total, 20 presets each)
| Style ID | Name | Description | Example Presets |
|----------|------|-------------|-----------------|
| action-arcade | Action/Arcade | Space shooters, platformers, fast action | Space Hero, Retro Runner, Tank Commander |
| puzzle | Puzzle | Match-3, sliding, brain teasers | Zen Garden, Geometric Dreams, Candy Match |
| story-adventure | Story Adventure | Visual novels, choose-your-adventure | Dragon's Quest, Enchanted Forest, Mystery Manor |
| racing | Racing | Car racing, running, speed games | Speed Demon, Neon Racer, Desert Rally |
| pet-simulator | Pet/Simulator | Virtual pets, farm games, life sim | Happy Farm, Pet Paradise, Garden Life |
| trivia-quiz | Trivia/Quiz | Quiz shows, knowledge tests | Brain Challenge, Quiz Master, Fact Hunter |
| music-rhythm | Music/Rhythm | Rhythm games, dance games | Disco Fever, Beat Drop, Neon Rhythm |
| creative-drawing | Creative/Drawing | Drawing, design, art creation | Art Studio, Color Splash, Pixel Creator |
| rpg-battle | RPG/Battle | Turn-based battles, dungeons | Dragon Slayer, Dark Dungeon, Hero's Quest |
| sports | Sports | Ball games, athletics | Stadium Glory, Court King, Field Champion |
| physics-pinball | Physics/Pinball | Pinball, physics puzzles | Pinball Wizard, Gravity Drop, Bounce Master |
### Preset Structure
Each preset defines a complete asset combination:
```javascript
{
id: "action-space-01",
gameStyle: "action-arcade",
name: "Space Hero",
description: "Epic space battle with your avatar piloting a fighter",
music: { mood: "Epic", energy: 9 },
background: { theme: "space", variant: "nebula" },
avatarContext: { mode: "HEAD_ONLY", context: "spaceship-cockpit" },
colorPalette: "space-blue",
tags: ["space", "shooter", "epic", "sci-fi"]
}
```
---
## Compatibility System
### Compatibility Levels
The system validates combinations across four levels:
| Level | Description |
|-------|-------------|
| **Excellent** | Perfect match, highly recommended |
| **Good** | Works well together |
| **Neutral** | Acceptable, no issues |
| **Poor** | May feel disjointed, suggests alternatives |
### Mood-Background Compatibility Matrix
```
| space | nature | urban | fantasy | spooky | colorful | retro |
-------------|-------|--------|-------|---------|--------|----------|-------|
Epic | E | G | G | E | N | N | N |
Chill | N | E | P | N | P | G | N |
Intense | G | N | E | N | G | P | G |
Playful | N | E | P | N | P | E | E |
Mysterious | N | G | G | E | E | P | P |
Heroic | E | G | E | E | N | P | N |
Quirky | P | G | N | N | P | E | E |
Ambient | N | E | P | N | P | G | N |
E = Excellent, G = Good, N = Neutral, P = Poor
```
### Validation API
```javascript
// Validate a combination
var result = AssetCompatibility.validate({
music: { mood: 'Epic' },
background: { theme: 'space' },
avatarContext: { context: 'spaceship-cockpit' }
});
// {
// valid: true,
// score: 95,
// issues: []
// }
// Get suggestions for improvement
var suggestions = AssetCompatibility.suggestImprovements(config);
```
---
## System Files
### Complete File Listing
```
/frontend/public/assets/
|
+-- assetManager.js ~45 KB Main coordinator, recommendations
+-- assetCatalog.js ~12 KB Unified queries, game styles
+-- compatibility.js ~35 KB Validation rules, suggestions
+-- presets.js ~65 KB 220 preset definitions
|
+-- music/
| +-- musicEngine.js ~25 KB Tone.js synthesis engine
| +-- musicLibrary.js ~80 KB 160 song definitions
| +-- moodCategories.js ~5 KB Mood metadata
|
+-- backgrounds/
| +-- backgroundEngine.js ~15 KB Canvas 2D render engine
| +-- backgroundCatalog.js ~8 KB Catalog queries
| +-- effects.js ~10 KB Shared visual effects
| +-- themes/
| +-- space.js ~12 KB 8 space variants
| +-- nature.js ~15 KB 8 nature variants
| +-- urban.js ~12 KB 8 urban variants
| +-- fantasy.js ~14 KB 8 fantasy variants
| +-- abstract.js ~10 KB 8 abstract variants
| +-- retro.js ~11 KB 8 retro variants
| +-- indoor.js ~13 KB 8 indoor variants
| +-- weather.js ~12 KB 8 weather variants
| +-- sports.js ~11 KB 8 sports variants
| +-- seasonal.js ~12 KB 8 seasonal variants
| +-- underwater.js ~13 KB 8 underwater variants
| +-- sky.js ~10 KB 8 sky variants
| +-- mechanical.js ~11 KB 8 mechanical variants
| +-- spooky.js ~12 KB 8 spooky variants
| +-- colorful.js ~10 KB 8 colorful variants
|
+-- avatar/
+-- avatarSystem.js ~20 KB Avatar management
+-- avatarRenderer.js ~25 KB Canvas 2D avatar drawing
+-- avatarData.js ~15 KB Customization options
+-- accessories.js ~18 KB 45 accessory definitions
+-- animations.js ~12 KB Animation system
+-- accessoryRenderer.js ~10 KB Accessory drawing
+-- contextCatalog.js ~8 KB Context queries
+-- contextRenderer.js ~10 KB Context rendering
+-- contexts/
+-- vehicles.js ~45 KB 12 vehicle contexts
+-- costumes.js ~50 KB 10 costume contexts
+-- pure.js ~15 KB 4 standard contexts
Total: ~650 KB (uncompressed JavaScript)
~150 KB (gzipped)
```
---
## Related Documentation
- [Music Catalog](MUSIC_CATALOG.md) - Complete song listing and synthesis details
- [Background Catalog](BACKGROUND_CATALOG.md) - All themes and variants
- [Avatar System](AVATAR_SYSTEM.md) - Customization options and rendering
- [Context Catalog](CONTEXT_CATALOG.md) - Vehicle, costume, and pure contexts
- [API Reference](API_REFERENCE.md) - Complete API documentation
- [Preset Guide](PRESET_GUIDE.md) - Using and creating presets
- [Game Generation Guide](GAME_GENERATION_GUIDE.md) - Integrating assets into games
- [Adding New Assets](ADDING_NEW_ASSETS.md) - Extending the library
---
## Interactive Tools
- **[Asset Browser](/asset-browser.html)** - Visual exploration of all assets, preview combinations
- **[Avatar Creator](/avatar-creator/index.html)** - Test avatar customization, try contexts
---
## API Quick Reference
### AssetManager
```javascript
// Initialization
AssetManager.initialize(options) // Promise - Initialize all systems
AssetManager.isInitialized() // boolean - Check ready state
// Recommendations
AssetManager.getRecommendations( // Array - Get top 3 recommendations
gameStyle, keywords, mood
)
// Detection Helpers
AssetManager.detectTheme(keywords) // string - Detect theme from keywords
AssetManager.detectMood(keywords) // string - Detect mood from keywords
AssetManager.detectMechanics(keywords) // Object - Detect avatar mode/context
// Preset Lookup
AssetManager.findPreset(gameStyle, index) // Object - Find preset by style
AssetManager.presetToConfig(preset) // Object - Convert to config
// Asset Loading
AssetManager.loadGameAssets(config) // Promise - Load assets
AssetManager.getLoadedAssets() // Object - Get current assets
// Validation
AssetManager.validateCombination(config) // Object - Validate compatibility
// Utilities
AssetManager.describeAssets(config) // string - Human description
AssetManager.getVariation(config, type) // Object - Get variation
AssetManager.searchAssets(query) // Object - Search all assets
AssetManager.getRandomCombination(style) // Object - Random combo
// Direct Subsystem Access
AssetManager.music() // MusicEngine
AssetManager.backgrounds() // BackgroundEngine
AssetManager.avatars() // AvatarSystem
AssetManager.contexts() // ContextRenderer
AssetManager.catalog() // AssetCatalog
AssetManager.presets() // AssetPresets
AssetManager.compatibility() // AssetCompatibility
```
### AssetCompatibility
```javascript
// Validation
validate(config) // Object - Full validation
checkFullCompatibility(mood, theme, ctx) // Object - Quick check
// Compatibility Queries
getMoodBackgroundCompat(mood, theme) // string - Compatibility level
getThemeContextCompat(theme, context) // string - Compatibility level
getContextAnimationCompat(context, anim) // string - Animation compat
getPaletteHarmony(palette1, palette2) // string - Color harmony
// Suggestions
getMusicForTheme(theme) // Array - Compatible moods
getBackgroundsForMood(mood) // Array - Compatible themes
getContextsForTheme(theme) // Array - Compatible contexts
getContextsForMechanics(mechanics) // Array - Mechanics-based
suggestImprovements(config) // Array - Improvement suggestions
// Animation Helpers
getRequiredAnimations(context) // Array - Required anims
getRecommendedAnimations(context) // Array - Recommended anims
getValidAnimations(context) // Object - All animation info
```
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2026-02-05 | Initial release with full asset library |
---
*GamerComp Asset Library - Procedural Game Assets for Everyone*
+734
View File
@@ -0,0 +1,734 @@
# Avatar System
## Overview
Mii-style avatar system with full customization and level-based unlockable accessories. The system supports four rendering modes optimized for different game types, with smooth animations and real-time accessory rendering.
## Architecture
The avatar system consists of several modules:
| Module | File | Purpose |
|--------|------|---------|
| AvatarData | `avatarData.js` | Customization options and guest avatars |
| AvatarSystem | `avatarSystem.js` | Main API for creation, rendering, accessories |
| AvatarRenderer | `avatarRenderer.js` | Canvas 2D rendering engine |
| AvatarAnimations | `animations.js` | Animation definitions per mode |
| AvatarAccessories | `accessories.js` | Unlockable accessories catalog |
| AccessoryRenderer | `accessoryRenderer.js` | Accessory rendering on avatars |
---
## Customization Options
### Face Shape (8 options)
| ID | Name | Description |
|----|------|-------------|
| `round` | Round | Circular face with soft jawline |
| `oval` | Oval | Classic elongated oval shape |
| `square` | Square | Strong angular jawline |
| `heart` | Heart | Wide forehead, pointed chin |
| `long` | Long | Narrow, elongated face |
| `wide` | Wide | Broader face shape |
| `angular` | Angular | Defined cheekbones, sharp features |
| `soft` | Soft | Rounded features throughout |
### Skin Tone (20 options)
| ID | Name | Hex Code |
|----|------|----------|
| `fair1` | Porcelain | `#FFECD1` |
| `fair2` | Ivory | `#FFE4C4` |
| `fair3` | Peach | `#FFDAB9` |
| `fair4` | Cream | `#FFD5B5` |
| `light1` | Light Beige | `#F5D0A9` |
| `light2` | Warm Beige | `#E8C49A` |
| `light3` | Sand | `#DEB887` |
| `medium1` | Golden | `#D4A373` |
| `medium2` | Honey | `#C9A06A` |
| `medium3` | Caramel | `#BC8F5F` |
| `medium4` | Toffee | `#B07D4B` |
| `tan1` | Tan | `#A67B5B` |
| `tan2` | Bronze | `#996B4D` |
| `tan3` | Cinnamon | `#8B6D4C` |
| `brown1` | Chestnut | `#7D5A3C` |
| `brown2` | Cocoa | `#6F4E37` |
| `brown3` | Espresso | `#5D4532` |
| `dark1` | Mocha | `#4E3B2D` |
| `dark2` | Ebony | `#3D2B1F` |
| `dark3` | Onyx | `#2C1F15` |
### Eye Shape (12 options)
| ID | Name | Description |
|----|------|-------------|
| `round` | Round | Large, circular eyes |
| `almond` | Almond | Classic almond shape |
| `oval` | Oval | Soft oval shape |
| `narrow` | Narrow | Slim, elongated eyes |
| `wide` | Wide | Broad, expressive eyes |
| `droopy` | Droopy | Downturned outer corners |
| `upturned` | Upturned | Lifted outer corners |
| `cat` | Cat | Sharp upturned cat-eye |
| `sleepy` | Sleepy | Heavy-lidded, relaxed look |
| `surprised` | Surprised | Wide open, alert |
| `angry` | Angry | Angled inward brow line |
| `happy` | Happy | Slightly squinted, joyful |
### Eye Color (16 options)
| ID | Name | Hex Code |
|----|------|----------|
| `brown` | Brown | `#634E34` |
| `darkBrown` | Dark Brown | `#3D2314` |
| `hazel` | Hazel | `#8B7355` |
| `green` | Green | `#3D7A3D` |
| `blue` | Blue | `#4682B4` |
| `lightBlue` | Light Blue | `#87CEEB` |
| `gray` | Gray | `#708090` |
| `amber` | Amber | `#B8860B` |
| `violet` | Violet | `#8B008B` |
| `emerald` | Emerald | `#50C878` |
| `iceBlue` | Ice Blue | `#A5D8E6` |
| `gold` | Gold | `#DAA520` |
| `red` | Red | `#8B0000` |
| `black` | Black | `#1A1A1A` |
| `honey` | Honey | `#C9A06A` |
| `turquoise` | Turquoise | `#40E0D0` |
**Total Eye Combinations**: 12 shapes x 16 colors = **192 unique eye options**
### Eyebrow Shape (10 options)
| ID | Name | Description |
|----|------|-------------|
| `natural` | Natural | Balanced, standard arch |
| `arched` | Arched | High dramatic arch |
| `straight` | Straight | Flat, horizontal brows |
| `thick` | Thick | Full, bold brows |
| `thin` | Thin | Delicate, fine brows |
| `angry` | Angry | Angled down toward center |
| `worried` | Worried | Angled up toward center |
| `bushy` | Bushy | Wild, untamed look |
| `sleek` | Sleek | Thin with elegant arch |
| `curved` | Curved | Pronounced curved shape |
### Nose Shape (8 options)
| ID | Name | Description |
|----|------|-------------|
| `button` | Button | Small, rounded tip |
| `pointed` | Pointed | Narrow with defined tip |
| `roman` | Roman | Prominent bridge curve |
| `snub` | Snub | Short with slight upturn |
| `wide` | Wide | Broader nostril base |
| `narrow` | Narrow | Thin, elongated bridge |
| `aquiline` | Aquiline | Pronounced curved bridge |
| `flat` | Flat | Low bridge, wide base |
### Mouth Shape (10 options)
| ID | Name | Description |
|----|------|-------------|
| `smile` | Smile | Gentle upward curve |
| `neutral` | Neutral | Relaxed, straight line |
| `grin` | Grin | Wide smile showing teeth |
| `smirk` | Smirk | Asymmetric half-smile |
| `pout` | Pout | Full lips, slight downturn |
| `open` | Open | Slightly parted lips |
| `small` | Small | Petite, compact mouth |
| `wide` | Wide | Broader mouth width |
| `thin` | Thin | Narrow lip fullness |
| `full` | Full | Plump, pronounced lips |
### Ear Shape (6 options)
| ID | Name | Description |
|----|------|-------------|
| `normal` | Normal | Standard ear shape and size |
| `small` | Small | Compact, closer to head |
| `large` | Large | Prominent, larger ears |
| `pointed` | Pointed | Elf-like pointed tips |
| `round` | Round | Soft, rounded lobes |
| `flat` | Flat | Close-set against head |
### Hair Style (30 options)
| ID | Name | Length | Coverage |
|----|------|--------|----------|
| `short-messy` | Short Messy | Short | 0.30 |
| `short-neat` | Short Neat | Short | 0.25 |
| `short-curly` | Short Curly | Short | 0.35 |
| `medium-straight` | Medium Straight | Medium | 0.50 |
| `medium-wavy` | Medium Wavy | Medium | 0.55 |
| `long-straight` | Long Straight | Long | 0.70 |
| `long-wavy` | Long Wavy | Long | 0.75 |
| `ponytail` | Ponytail | Medium | 0.40 |
| `pigtails` | Pigtails | Medium | 0.45 |
| `bun` | Bun | Long | 0.35 |
| `mohawk` | Mohawk | Short | 0.20 |
| `afro` | Afro | Medium | 0.80 |
| `buzz` | Buzz Cut | Short | 0.15 |
| `bald` | Bald | None | 0.00 |
| `sidepart` | Side Part | Short | 0.30 |
| `spiky` | Spiky | Short | 0.35 |
| `slicked` | Slicked Back | Short | 0.25 |
| `bob` | Bob | Medium | 0.50 |
| `pixie` | Pixie | Short | 0.35 |
| `braids` | Braids | Long | 0.60 |
| `dreads` | Dreads | Long | 0.70 |
| `undercut` | Undercut | Short | 0.25 |
| `fauxhawk` | Faux Hawk | Short | 0.30 |
| `mullet` | Mullet | Medium | 0.45 |
| `bowl` | Bowl Cut | Medium | 0.55 |
| `combover` | Comb Over | Short | 0.20 |
| `bangs` | Bangs | Medium | 0.50 |
| `sidesweep` | Side Sweep | Medium | 0.45 |
| `topknot` | Top Knot | Long | 0.30 |
| `cornrows` | Cornrows | Medium | 0.40 |
### Hair Color (20 options)
| ID | Name | Hex Code | Type |
|----|------|----------|------|
| `black` | Black | `#1A1A1A` | Natural |
| `darkBrown` | Dark Brown | `#2C1810` | Natural |
| `brown` | Brown | `#4A3728` | Natural |
| `lightBrown` | Light Brown | `#7A5A3A` | Natural |
| `auburn` | Auburn | `#8B4513` | Natural |
| `ginger` | Ginger | `#B5651D` | Natural |
| `strawberry` | Strawberry | `#C67171` | Natural |
| `blonde` | Blonde | `#D4B896` | Natural |
| `platinum` | Platinum | `#E8E4C9` | Natural |
| `white` | White | `#F0F0F0` | Natural |
| `gray` | Gray | `#808080` | Natural |
| `red` | Red | `#CC0000` | Fantasy |
| `blue` | Blue | `#0066CC` | Fantasy |
| `purple` | Purple | `#7B2D8E` | Fantasy |
| `green` | Green | `#228B22` | Fantasy |
| `pink` | Pink | `#FF69B4` | Fantasy |
| `teal` | Teal | `#008B8B` | Fantasy |
| `orange` | Orange | `#FF6600` | Fantasy |
| `silver` | Silver | `#C0C0C0` | Fantasy |
| `rainbow` | Rainbow | Gradient | Fantasy |
### Facial Hair (10 options)
| ID | Name | Coverage | Description |
|----|------|----------|-------------|
| `none` | None | - | Clean shaven |
| `stubble` | Stubble | Light | 5 o'clock shadow |
| `goatee` | Goatee | Chin | Chin beard only |
| `mustache` | Mustache | Upper lip | Classic mustache |
| `fullBeard` | Full Beard | Full face | Complete beard coverage |
| `shortBeard` | Short Beard | Full face | Trimmed full beard |
| `soulPatch` | Soul Patch | Chin center | Small chin patch |
| `mutton` | Mutton Chops | Sides | Sideburn-connected style |
| `handlebar` | Handlebar | Upper lip | Curled mustache tips |
| `vandyke` | Van Dyke | Chin + mustache | Goatee with mustache |
### Body Type (6 options)
| ID | Name | Width Ratio | Height Ratio |
|----|------|-------------|--------------|
| `slim` | Slim | 0.80 | 1.00 |
| `average` | Average | 1.00 | 1.00 |
| `athletic` | Athletic | 1.10 | 1.00 |
| `stocky` | Stocky | 1.20 | 0.95 |
| `tall` | Tall | 0.95 | 1.15 |
| `short` | Short | 1.00 | 0.85 |
### Clothing Style (4 options)
| ID | Name | Has Collar | Sleeve Length | Special |
|----|------|------------|---------------|---------|
| `casual` | Casual | No | Short | - |
| `sporty` | Sporty | No | Short | Stripes |
| `formal` | Formal | Yes | Long | Buttons |
| `scifi` | Sci-Fi | Yes | Long | Glow effects |
### Clothing Color (20 preset colors)
```
#E74C3C #E67E22 #F1C40F #2ECC71 #1ABC9C
#3498DB #9B59B6 #34495E #95A5A6 #ECF0F1
#C0392B #D35400 #F39C12 #27AE60 #16A085
#2980B9 #8E44AD #2C3E50 #7F8C8D #BDC3C7
```
---
## Earned Accessories
### Rarity System
| Rarity | Color | Level Range | Hex Code |
|--------|-------|-------------|----------|
| Common | Gray | 1-15 | `#9D9D9D` |
| Uncommon | Green | 16-30 | `#1EFF00` |
| Rare | Blue | 31-50 | `#0070DD` |
| Epic | Purple | 51-70 | `#A335EE` |
| Legendary | Orange | 71-90 | `#FF8000` |
| Mythic | Gold | 91-100 | `#E6CC80` |
### Eyewear (15 items)
| ID | Name | Unlock Level | Rarity | Has Effect | Description |
|----|------|--------------|--------|------------|-------------|
| `sunglasses-classic` | Classic Sunglasses | 5 | Common | No | Timeless shades for any occasion |
| `aviator` | Aviator Glasses | 10 | Common | No | Top Gun approved eyewear |
| `nerd-glasses` | Nerd Glasses | 15 | Common | No | Intelligence +100 (cosmetic only) |
| `3d-glasses` | 3D Glasses | 20 | Uncommon | No | See the world in a whole new dimension |
| `goggles` | Goggles | 25 | Uncommon | No | Ready for adventure or science experiments |
| `monocle` | Monocle | 30 | Uncommon | No | Distinguished and sophisticated |
| `star-glasses` | Star Glasses | 35 | Rare | No | You are a star! |
| `heart-glasses` | Heart Glasses | 40 | Rare | No | Spread the love wherever you go |
| `sport-visor` | Sport Visor | 45 | Rare | No | Professional gamer gear |
| `vr-headset` | VR Headset | 50 | Epic | No | Immerse yourself in virtual worlds |
| `cyber-visor` | Cyber Visor | 55 | Epic | No | Sleek futuristic eye protection |
| `x-ray-specs` | X-Ray Specs | 60 | Epic | Yes | See through walls... probably |
| `laser-eyes` | Laser Eyes | 70 | Legendary | Yes | Pew pew! Lasers shoot from your eyes |
| `diamond-glasses` | Diamond Glasses | 80 | Legendary | No | Pure crystallized luxury |
| `cosmic-lenses` | Cosmic Lenses | 90 | Mythic | Yes | Gaze upon the infinite universe |
### Headwear (20 items)
| ID | Name | Unlock Level | Rarity | Has Effect | Description |
|----|------|--------------|--------|------------|-------------|
| `baseball-cap` | Baseball Cap | 3 | Common | No | A classic casual cap |
| `backwards-cap` | Backwards Cap | 8 | Common | No | Cool kids wear it backwards |
| `beanie` | Beanie | 12 | Common | No | Cozy knitted headwear |
| `party-hat` | Party Hat | 18 | Uncommon | No | Every day is a celebration! |
| `chef-hat` | Chef Hat | 22 | Uncommon | No | Master of the kitchen |
| `crown-bronze` | Bronze Crown | 25 | Uncommon | No | A humble crown for rising royalty |
| `hard-hat` | Hard Hat | 26 | Uncommon | No | Safety first on the job site |
| `wizard-hat` | Wizard Hat | 30 | Rare | No | Channel your inner sorcerer |
| `pirate-hat` | Pirate Hat | 35 | Rare | No | Arrr! Set sail for adventure |
| `santa-hat` | Santa Hat | 38 | Rare | No | Ho ho ho! Spread holiday cheer |
| `crown-silver` | Silver Crown | 40 | Rare | No | A noble crown for dedicated players |
| `graduation-cap` | Graduation Cap | 42 | Rare | No | Scholar of the gaming arts |
| `bunny-ears` | Bunny Ears | 45 | Rare | No | Hop into the fun! |
| `devil-horns` | Devil Horns | 48 | Rare | No | A little mischief never hurt anyone |
| `halo` | Halo | 52 | Epic | Yes | An angelic golden ring of light |
| `top-hat` | Top Hat | 56 | Epic | No | Dapper and distinguished |
| `crown-gold` | Gold Crown | 60 | Epic | No | True royalty in golden splendor |
| `viking-helmet` | Viking Helmet | 62 | Epic | No | Conquer games like a Norse warrior |
| `crown-diamond` | Diamond Crown | 80 | Legendary | Yes | A magnificent crown of pure diamonds |
| `crown-cosmic` | Cosmic Crown | 100 | Mythic | Yes | The ultimate crown, forged from stardust |
### Effects/Auras (10 items)
| ID | Name | Unlock Level | Rarity | Effect Type | Color | Description |
|----|------|--------------|--------|-------------|-------|-------------|
| `sparkle-trail` | Sparkle Trail | 15 | Uncommon | Particles | `#FFD700` | Leave a trail of golden sparkles |
| `fire-aura` | Fire Aura | 25 | Rare | Glow | `#FF4500` | Surrounded by fierce flames |
| `lightning-crackle` | Lightning Crackle | 35 | Rare | Electric | `#00BFFF` | Electricity crackles around you |
| `rainbow-glow` | Rainbow Glow | 45 | Epic | Glow | Rainbow | Radiate all colors of the spectrum |
| `star-particles` | Star Particles | 55 | Epic | Particles | `#FFFF00` | Tiny stars orbit around you |
| `ice-crystals` | Ice Crystals | 65 | Legendary | Particles | `#87CEEB` | Frozen crystals shimmer around you |
| `shadow-effect` | Shadow Effect | 70 | Legendary | Shadow | `#2F2F4F` | Darkness follows your every move |
| `golden-glow` | Golden Glow | 75 | Legendary | Glow | `#FFD700` | Bathe in pure golden light |
| `void-aura` | Void Aura | 85 | Mythic | Void | `#4B0082` | The void itself surrounds you |
| `cosmic-trail` | Cosmic Trail | 95 | Mythic | Cosmic | Cosmic | Trail galaxies and nebulae in your wake |
**Total Accessories**: 15 + 20 + 10 = **45 unlockable items**
---
## Rendering Modes
### HEAD_ONLY (64x64 pixels)
**Best for**: Cockpit views, flying games (Flappy Bird style), maze games, puzzle games
**Shows**: Face, hair, ears, accessories (eyewear, headwear, effects)
**Animations** (5):
| ID | Name | Frames | Duration | Loop | Description |
|----|------|--------|----------|------|-------------|
| `bob` | Bob | 3 | 150ms | Yes | Gentle up/down bobbing motion |
| `flap` | Flap | 2 | 80ms | Yes | Flapping motion with head squash/stretch |
| `blink` | Blink | 3 | 60ms | No | Eye blink animation |
| `celebrate` | Celebrate | 4 | 100ms | No | Happy bouncing celebration |
| `hurt` | Hurt | 2 | 80ms | No | Damage reaction shake |
### HEAD_AND_SHOULDERS (64x96 pixels)
**Best for**: Driving games, conversation scenes, submarine games, portrait views
**Shows**: Head, neck, shoulders, clothing collar
**Animations** (6):
| ID | Name | Frames | Duration | Loop | Description |
|----|------|--------|----------|------|-------------|
| `idle` | Idle | 3 | 400ms | Yes | Subtle breathing motion |
| `turn-left` | Turn Left | 2 | 100ms | No | Head turns to the left |
| `turn-right` | Turn Right | 2 | 100ms | No | Head turns to the right |
| `look-up` | Look Up | 2 | 100ms | No | Head tilts upward |
| `celebrate` | Celebrate | 3 | 120ms | No | Bounce and smile celebration |
| `hurt` | Hurt | 2 | 100ms | No | Recoil damage reaction |
### UPPER_BODY (64x128 pixels)
**Best for**: Racing games, shooting games, hoverboard games, action sequences
**Shows**: Head, neck, shoulders, torso, arms, hands
**Animations** (8):
| ID | Name | Frames | Duration | Loop | Description |
|----|------|--------|----------|------|-------------|
| `idle` | Idle | 3 | 400ms | Yes | Breathing with subtle arm sway |
| `wave` | Wave | 4 | 120ms | No | Friendly arm wave gesture |
| `point` | Point | 3 | 100ms | No | Pointing forward gesture |
| `steer-left` | Steer Left | 2 | 80ms | No | Leaning left for racing/steering |
| `steer-right` | Steer Right | 2 | 80ms | No | Leaning right for racing/steering |
| `shoot` | Shoot | 3 | 60ms | No | Arm extends forward shooting motion |
| `celebrate` | Celebrate | 4 | 120ms | No | Arms up victory celebration |
| `hurt` | Hurt | 2 | 100ms | No | Recoil backwards damage reaction |
### FULL_BODY (64x160 pixels)
**Best for**: Platformers, RPGs, sports games, adventure games, fighting games
**Shows**: Complete character with head, body, arms, legs, shoes
**Animations** (12):
| ID | Name | Frames | Duration | Loop | Description |
|----|------|--------|----------|------|-------------|
| `idle` | Idle | 3 | 400ms | Yes | Standing with breathing motion |
| `walk` | Walk Cycle | 4 | 100ms | Yes | Walking with arm/leg swing |
| `run` | Run Cycle | 6 | 60ms | Yes | Running with full body movement |
| `jump` | Jump | 3 | 100ms | No | Crouch, leap, stretch sequence |
| `fall` | Fall | 2 | 150ms | Yes | Arms up, legs down falling pose |
| `crouch` | Crouch | 2 | 80ms | No | Duck down into crouch position |
| `celebrate` | Celebrate | 6 | 100ms | No | Jump and arm wave celebration |
| `attack` | Attack | 4 | 60ms | No | Punch or swing attack motion |
| `hurt` | Hurt | 3 | 80ms | No | Stagger backwards damage reaction |
| `climb` | Climb | 4 | 120ms | Yes | Climbing ladder/wall motion |
| `swim` | Swim | 4 | 150ms | Yes | Swimming stroke animation |
| `fly` | Fly | 4 | 100ms | Yes | Superhero flying pose |
---
## API Reference
### Creating Avatars
```javascript
// Create avatar with custom options
const avatar = AvatarSystem.create({
faceShape: 'round',
skinTone: '#FFD5B8',
eyes: { shape: 'round', color: '#4A90D9' },
eyebrows: 'natural',
nose: 'button',
mouth: 'smile',
ears: 'normal',
hair: { style: 'short-messy', color: '#3D2314' },
facialHair: 'none',
body: { type: 'average' },
clothing: { style: 'casual', shirtColor: '#3498DB', pantsColor: '#2C3E50' }
});
// Create default avatar
const defaultAvatar = AvatarSystem.createDefault();
// Get random guest avatar
const guestAvatar = AvatarSystem.getGuestAvatar();
```
### Loading and Saving
```javascript
// Load user's saved avatar
const avatar = await AvatarSystem.loadUserAvatar(userId);
// Save avatar
AvatarSystem.saveAvatar(avatar);
// Clear cache
AvatarSystem.clearCache(userId); // Clear specific user
AvatarSystem.clearCache(); // Clear all
```
### Rendering
```javascript
// Render to canvas context
AvatarSystem.render(avatar, 'FULL_BODY', ctx, x, y, {
scale: 1,
animation: 'walk',
frame: 0
});
// Render to offscreen canvas
const canvas = AvatarSystem.renderToCanvas(avatar, 'HEAD_ONLY', { scale: 2 });
// Render to data URL (for images)
const dataUrl = AvatarSystem.renderToDataURL(avatar, 'HEAD_AND_SHOULDERS');
// Get mode dimensions
const size = AvatarSystem.getModeSize('FULL_BODY');
// Returns: { width: 64, height: 160 }
```
### Animation Control
```javascript
// Start animation
AvatarSystem.startAnimation(avatarId, 'walk', {
loop: true,
mode: 'FULL_BODY',
onComplete: () => console.log('Animation finished')
});
// Stop animation
AvatarSystem.stopAnimation(avatarId);
// Get current animation state
const state = AvatarSystem.getAnimationState(avatarId);
// Returns: { animation: 'walk', frame: 2, complete: false, transforms: {...} }
// Check if animating
const isAnimating = AvatarSystem.isAnimating(avatarId);
// Get animation frame transforms directly
const transforms = AvatarSystem.animate(avatar, 'run', frameNumber);
```
### Accessory Management
```javascript
// Equip accessory
AvatarSystem.equipAccessory(avatar, 'headwear', 'crown-gold');
AvatarSystem.equipAccessory(avatar, 'eyewear', 'sunglasses-classic');
AvatarSystem.equipAccessory(avatar, 'effect', 'sparkle-trail');
// Unequip accessory
AvatarSystem.unequipAccessory(avatar, 'headwear');
// Unlock/award accessory
const newlyUnlocked = AvatarSystem.unlockAccessory(avatar, 'vr-headset');
// Returns: true if newly unlocked, false if already owned
// Check if avatar has accessory
const hasItem = AvatarSystem.hasAccessory(avatar, 'crown-gold');
// Get earned accessories (full objects)
const earned = AvatarSystem.getEarnedAccessories(avatar);
// Returns: { eyewear: [...], headwear: [...], effects: [...] }
// Get equipped accessories (full objects)
const equipped = AvatarSystem.getEquippedAccessories(avatar);
// Returns: { eyewear: {...}, headwear: {...}, effect: {...} }
// Get full accessory catalog
const catalog = AvatarSystem.getAccessoryCatalog();
```
### Level Unlocks
```javascript
// Check what unlocks at a specific level
const newItems = AvatarSystem.checkLevelUnlocks(avatar, 50);
// Returns: Array of newly unlocked accessory objects
// Add XP and check for level ups
const result = AvatarSystem.addXP(avatar, 500);
// Returns: { newLevel: 12, leveledUp: true, unlockedItems: [...] }
// Get XP progress info
const xpInfo = AvatarSystem.getXPInfo(avatar);
// Returns: { level, totalXP, currentLevelXP, xpNeededForLevel, progress }
```
### Game Integration
```javascript
// Export avatar for use in a game
const gameAvatar = AvatarSystem.exportForGame(avatar, 'FULL_BODY');
// Returns object with render() and animate() methods
// Use in game loop
gameAvatar.render(ctx, x, y, { animation: 'run', frame: frameNum });
const animInfo = gameAvatar.getAnimationInfo('run');
// Export minimal data for network transmission
const minimal = AvatarSystem.exportMinimal(avatar);
// Import from minimal data
const fullAvatar = AvatarSystem.importMinimal(minimalData, userId);
// Create sprite sheet for performance
const spriteSheet = AvatarSystem.createSpriteSheet(avatar, 'FULL_BODY', ['idle', 'walk', 'run']);
// Returns: { canvas, frameWidth, frameHeight, getFrameRect(animId, frame) }
```
### Utility Functions
```javascript
// Clone avatar
const cloned = AvatarSystem.clone(avatar);
// Update base properties
AvatarSystem.updateBase(avatar, { hair: { style: 'afro', color: '#000000' } });
// Validate avatar data
const isValid = AvatarSystem.validate(avatarData);
// Compare avatars
const areEqual = AvatarSystem.equals(avatar1, avatar2);
// Get all customization options
const options = AvatarSystem.getCustomizationOptions();
// Returns: { faceShapes, skinTones, eyeShapes, eyeColors, ... }
// Check if guest avatar
const isGuest = AvatarSystem.isGuest(avatar);
// Convert guest to registered user
const userAvatar = AvatarSystem.convertGuestToUser(guestAvatar, newUserId);
// Get version info
const version = AvatarSystem.getVersion();
```
---
## Guest Avatars
10 pre-defined guest avatars are available for non-logged-in users. Each has a unique appearance:
| ID | Name | Skin Tone | Hair Style | Eye Color | Clothing Color |
|----|------|-----------|------------|-----------|----------------|
| `guest_1` | Guest Blue | Medium | Short Messy | Blue | `#3498DB` |
| `guest_2` | Guest Red | Light | Long Straight | Brown | `#E74C3C` |
| `guest_3` | Guest Green | Tan | Buzz Cut | Dark Brown | `#2ECC71` |
| `guest_4` | Guest Purple | Fair | Bob | Green | `#9B59B6` |
| `guest_5` | Guest Orange | Brown | Afro | Amber | `#E67E22` |
| `guest_6` | Guest Teal | Dark | Dreads | Dark Brown | `#1ABC9C` |
| `guest_7` | Guest Gray | Light | Undercut | Gray | `#95A5A6` |
| `guest_8` | Guest Yellow | Medium | Spiky | Hazel | `#F1C40F` |
| `guest_9` | Guest Navy | Tan | Comb Over | Brown | `#2C3E50` |
| `guest_10` | Guest Pink | Fair | Pigtails | Light Blue | `#FF69B4` |
```javascript
// Get random guest avatar
const guest = AvatarSystem.getGuestAvatar();
// Or use AvatarData directly
const guestData = AvatarData.getRandomGuestAvatar();
```
---
## XP and Leveling System
### XP Per Level Formula
```
XP needed for level N = 100 + (N - 1) * 50
```
| Level | XP Needed | Cumulative XP |
|-------|-----------|---------------|
| 1 | 100 | 100 |
| 2 | 150 | 250 |
| 3 | 200 | 450 |
| 5 | 300 | 1,000 |
| 10 | 550 | 3,500 |
| 25 | 1,300 | 18,250 |
| 50 | 2,550 | 68,000 |
| 75 | 3,800 | 148,500 |
| 100 | 5,050 | 259,750 |
### Unlock Schedule
All accessories unlock by reaching specific levels:
- **Levels 3-15**: 6 Common items
- **Levels 18-30**: 8 Uncommon items
- **Levels 35-48**: 10 Rare items
- **Levels 50-62**: 8 Epic items
- **Levels 65-80**: 6 Legendary items
- **Levels 85-100**: 4 Mythic items
---
## Accessory Animation Rules
Accessories follow specific animation behavior:
```javascript
{
headwear: {
followHead: true, // Moves with head
bounceMultiplier: 1.2, // Extra bounce on jumps
rotationDamping: 0.8 // Slightly less rotation than head
},
eyewear: {
followHead: true, // Moves with head
bounceMultiplier: 1.0, // Same bounce as head
rotationDamping: 1.0 // Matches head rotation exactly
},
effects: {
independent: true, // Own animation cycle
particleSpeed: 1.0 // Particle emission rate
}
}
```
---
## Performance Tips
### Sprite Caching
For games with many avatars or frequent redraws:
```javascript
// Pre-render to sprite cache
const cache = AvatarRenderer.createSpriteCache(avatars, 'HEAD_ONLY', 1);
// Render from cache (much faster)
AvatarRenderer.renderFromCache(ctx, cache, avatarId, x, y);
```
### Sprite Sheets
For animation-heavy games:
```javascript
const sheet = AvatarSystem.createSpriteSheet(avatar, 'FULL_BODY', ['idle', 'walk', 'run', 'jump']);
// In game loop
const rect = sheet.getFrameRect('walk', currentFrame);
ctx.drawImage(sheet.image, rect.x, rect.y, rect.width, rect.height, destX, destY, rect.width, rect.height);
```
### Batch Rendering
For many avatars on screen:
```javascript
AvatarRenderer.renderBatch(ctx, avatars, 'HEAD_ONLY', positions);
// Automatically skips off-screen avatars
```
---
## File Dependencies
Load order matters. Include scripts in this order:
```html
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
```
Or use the asset manager which handles loading automatically.
+470
View File
@@ -0,0 +1,470 @@
# Background Catalog
Complete catalog of all 120 procedural backgrounds available in the GamerComp game generation system.
**15 themes x 8 variants = 120 unique backgrounds**
---
## Table of Contents
1. [Overview](#overview)
2. [Theme Categories](#theme-categories)
- [Space](#space-theme)
- [Nature](#nature-theme)
- [Urban](#urban-theme)
- [Fantasy](#fantasy-theme)
- [Abstract](#abstract-theme)
- [Retro](#retro-theme)
- [Indoor](#indoor-theme)
- [Weather](#weather-theme)
- [Sports](#sports-theme)
- [Seasonal](#seasonal-theme)
- [Underwater](#underwater-theme)
- [Sky](#sky-theme)
- [Mechanical](#mechanical-theme)
- [Spooky](#spooky-theme)
- [Colorful](#colorful-theme)
3. [Compatible Music Moods](#compatible-music-moods)
4. [Compatible Avatar Contexts](#compatible-avatar-contexts)
5. [API Usage](#api-usage)
6. [Visual Preview Guide](#visual-preview-guide)
---
## Overview
All backgrounds are procedurally generated using Canvas 2D rendering. Each background features:
- **Animated elements** - Smooth animations driven by time parameter
- **Layered rendering** - Base layer, mid layer, and particle effects
- **Customizable colors** - 4-color palette per background
- **Particle systems** - Optional ambient particles (stars, rain, sparkles, etc.)
- **Mood-based filtering** - Find backgrounds by emotional tone
- **Game genre recommendations** - Suggested game types for each background
### Particle Types Available
| Type | Description |
|------|-------------|
| `stars` | Twinkling star field |
| `rain` | Falling rain drops |
| `snow` | Gentle snowfall |
| `particles` | Generic floating particles |
| `fog` | Drifting fog wisps |
| `bubbles` | Rising bubbles |
| `leaves` | Falling leaves |
| `sparkles` | Glittering sparkles |
| `fireflies` | Glowing fireflies |
| `dust` | Floating dust motes |
---
## Theme Categories
### Space Theme
Cosmic environments featuring nebulas, planets, and interstellar phenomena.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Nebula | `space_nebula` | Swirling nebula clouds with twinkling stars | wonder | `#0B0B2B` `#1A0A3E` `#3D1A78` `#7B2FBE` | sparkles | puzzle, story, creative |
| Asteroid Field | `space_asteroidField` | Rocky asteroids drifting through dark space | tense | `#0A0A14` `#1C1C2E` `#4A3A2A` `#8B7355` | dust | action, racing, sports |
| Deep Space | `space_deepSpace` | The void of deep space with distant starlight | serene | `#000005` `#050510` `#0A0A20` `#111133` | stars | puzzle, story, rpg |
| Planets | `space_planets` | Colorful planets against a starry backdrop | wonder | `#0B0B2B` `#0F1640` `#CC5533` `#44BBAA` | sparkles | creative, rpg, story |
| Space Station | `space_spaceStation` | Orbital station with geometric structures | focused | `#0A0A1E` `#151530` `#3A3A55` `#88AACC` | dust | puzzle, action, physics |
| Warp Speed | `space_warpSpeed` | Light streaks from faster-than-light travel | energetic | `#000010` `#0A0A30` `#2244AA` `#66BBFF` | particles | racing, action, sports |
| Satellites | `space_satellites` | Satellites orbiting above a dark Earth | calm | `#060615` `#101030` `#334466` `#AACCDD` | stars | puzzle, trivia, creative |
| Black Hole | `space_blackHole` | A black hole bending light around its event horizon | intense | `#000000` `#0A0510` `#220A33` `#FF6622` | particles | action, rpg, physics |
---
### Nature Theme
Natural outdoor environments from forests to oceans.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Forest | `nature_forest` | A dense forest with layered canopy and dappled light | peaceful | `#1A3320` `#2D5A27` `#4A7A3F` `#8FB86A` | fireflies | story, rpg, puzzle |
| Jungle | `nature_jungle` | Thick tropical jungle with hanging vines and humid air | adventurous | `#0D2B0D` `#1A4D1A` `#2D7A2D` `#55AA33` | fireflies | action, rpg, pet |
| Desert | `nature_desert` | Rolling sand dunes under a blazing sun | warm | `#F4A460` `#DAA06D` `#C2955A` `#8B6914` | dust | racing, action, rpg |
| Mountains | `nature_mountains` | Snow-capped mountain peaks under sweeping skies | majestic | `#2C3E50` `#4A6B5A` `#7A9B6B` `#FFFFFF` | fog | rpg, sports, story |
| Ocean | `nature_ocean` | Vast ocean stretching to the horizon at sunset | serene | `#1A5276` `#2E86C1` `#5DADE2` `#AED6F1` | fog | puzzle, story, creative |
| Underwater | `nature_underwater` | Deep beneath the waves with shafts of sunlight | mystical | `#0A2342` `#1A4B6E` `#2E86C1` `#48D1CC` | bubbles | puzzle, pet, creative |
| Waterfall | `nature_waterfall` | A cascading waterfall in a lush green cliff | refreshing | `#1B4332` `#2D6A4F` `#52B788` `#B7E4C7` | fog | puzzle, creative, story |
| Meadow | `nature_meadow` | A sunny meadow with wildflowers and gentle breeze | joyful | `#4CAF50` `#81C784` `#A5D6A7` `#FFF59D` | leaves | pet, creative, story, puzzle |
---
### Urban Theme
City environments from skylines to subways.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| City Skyline | `urban_citySkyline` | City skyline silhouetted against a glowing sunset | inspiring | `#1A1A2E` `#2D2D44` `#4A4A66` `#FF8844` | dust | story, rpg, creative |
| Street | `urban_street` | Busy city street with lamp posts and passing lights | busy | `#2C3E50` `#566573` `#808B96` `#FFD700` | dust | racing, action, sports |
| Rooftop | `urban_rooftop` | Rooftop view over a twinkling nighttime city | reflective | `#1B2631` `#2C3E50` `#5D6D7E` `#F4D03F` | fireflies | story, puzzle, creative |
| Neon District | `urban_neonDistrict` | Rain-slicked streets glowing with neon signs | electric | `#0D0D1A` `#1A0A2E` `#FF00FF` `#00FFCC` | rain | action, racing, music |
| Subway | `urban_subway` | Underground subway platform with arriving train lights | gritty | `#1C1C1C` `#333333` `#555555` `#FFCC00` | dust | action, puzzle, rpg |
| Park | `urban_park` | A peaceful city park with trees and walking paths | relaxed | `#2E7D32` `#4CAF50` `#81C784` `#795548` | leaves | pet, puzzle, story, creative |
| Mall | `urban_mall` | Bright mall interior with shop windows and escalators | lively | `#E8E8E8` `#CCCCCC` `#888888` `#FF6B6B` | sparkles | trivia, creative, pet |
| Alley | `urban_alley` | A narrow alley with a single warm light overhead | mysterious | `#111118` `#1E1E28` `#333340` `#BB8844` | fog | rpg, action, story |
---
### Fantasy Theme
Magical and fantastical environments.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Castle | `fantasy_castle` | A grand castle silhouette against a twilight sky with golden banners | heroic | `#1a1040` `#2d1b69` `#8b7355` `#ffd700` | fireflies | rpg, action, story |
| Dungeon | `fantasy_dungeon` | Dark stone corridors lit by flickering torches and glowing embers | dark | `#0a0a0f` `#1a1520` `#4a3728` `#ff6a00` | particles | rpg, action, puzzle |
| Enchanted Forest | `fantasy_enchantedForest` | A mystical forest with glowing mushrooms and magical light | mystical | `#0a1a0f` `#0d2818` `#1a4a2a` `#44ff88` | fireflies | rpg, story, puzzle |
| Floating Islands | `fantasy_floatingIslands` | Majestic islands suspended in a violet sky with waterfalls | wonder | `#1a0a3a` `#2d1b69` `#5a8a5a` `#88ccff` | sparkles | rpg, puzzle, creative |
| Magical Library | `fantasy_magicalLibrary` | Towering bookshelves with floating magical tomes and arcane symbols | scholarly | `#1a0f0a` `#2d1a10` `#6b4423` `#cc88ff` | sparkles | puzzle, trivia, story |
| Crystal Cave | `fantasy_crystalCave` | An underground cavern filled with luminous crystal formations | ethereal | `#0a0a1a` `#0f1a2d` `#225588` `#44ddff` | sparkles | puzzle, rpg, creative |
| Temple | `fantasy_temple` | An ancient temple with towering marble pillars and sacred golden light | sacred | `#1a1025` `#2d1a3a` `#c9b77a` `#ffd700` | dust | rpg, story, trivia |
| Tower | `fantasy_tower` | A tall wizard tower under a starlit sky with arcane energy | arcane | `#0a0a20` `#151540` `#554488` `#aa66ff` | sparkles | rpg, puzzle, trivia |
---
### Abstract Theme
Non-representational visual patterns and effects.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Geometric Patterns | `abstract_geometricPatterns` | Interlocking geometric shapes in warm contrasting tones | focused | `#0d1b2a` `#1b2838` `#e07a5f` `#f2cc8f` | dust | puzzle, trivia, creative |
| Gradients | `abstract_gradients` | Smoothly blending gradient orbs with ethereal color transitions | dreamy | `#0f0c29` `#302b63` `#24243e` `#ff6b6b` | sparkles | creative, story, music |
| Particle Fields | `abstract_particleFields` | A dynamic network of connected particle nodes with pulsing energy | energetic | `#000000` `#111122` `#00ff88` `#0088ff` | particles | action, puzzle, physics |
| Minimalist | `abstract_minimalist` | Clean open space with a single bold accent line | calm | `#f5f5f0` `#e8e4dd` `#2d2d2d` `#c44536` | none | puzzle, trivia, creative |
| Waves | `abstract_waves` | Layered sinusoidal waves undulating in cool blue tones | flowing | `#0a192f` `#172a45` `#00b4d8` `#90e0ef` | bubbles | creative, music, physics |
| Fractals | `abstract_fractals` | Recursive branching patterns that grow and pulse | complex | `#0a0a0a` `#1a1a2e` `#e94560` `#0f3460` | sparkles | puzzle, physics, creative |
| Kaleidoscope | `abstract_kaleidoscope` | Symmetric radial patterns rotating like a living kaleidoscope | playful | `#1a1a2e` `#16213e` `#e94560` `#ffd166` | sparkles | music, creative, puzzle |
| Matrix | `abstract_matrix` | Cascading columns of digital characters streaming down | digital | `#000000` `#001100` `#00ff41` `#008f11` | none | action, puzzle, trivia |
---
### Retro Theme
Nostalgic gaming aesthetics from past eras.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Pixel Grid | `retro_pixelGrid` | A chunky pixel grid with classic 8-bit color blocks | nostalgic | `#1a1a2e` `#16213e` `#00ff88` `#ff6600` | sparkles | action, puzzle, creative |
| Arcade Cabinet | `retro_arcadeCabinet` | Inside view of a dark arcade with neon-lit cabinet edges | fun | `#0a0a15` `#1a0a2e` `#ff2277` `#ffcc00` | dust | action, racing, sports |
| CRT Effects | `retro_crtEffects` | Authentic CRT monitor with phosphor glow and scan line interference | vintage | `#0a0a0a` `#0f1a0f` `#33ff33` `#88ff88` | none | action, puzzle, trivia |
| Vaporwave | `retro_vaporwave` | Aesthetic sunset horizon with perspective grid and palm silhouettes | aesthetic | `#0d0221` `#261447` `#ff71ce` `#01cdfe` | stars | creative, music, racing |
| Scanlines | `retro_scanlines` | Dense horizontal scan lines over a shifting color field | technical | `#0a0a1a` `#141428` `#00aaff` `#ff4444` | none | action, puzzle, physics |
| Glitch | `retro_glitch` | Fragmented display with chromatic aberration and corrupted data | chaotic | `#0a0a0a` `#1a0a1a` `#ff0055` `#00ffcc` | particles | action, racing, physics |
| 8-Bit | `retro_eightBit` | Charming 8-bit scene with blocky pixel clouds and hills | cheerful | `#2b2b6e` `#3d3d94` `#5bc0eb` `#fde74c` | none | action, puzzle, creative |
| Synthwave | `retro_synthwave` | Outrun-style neon horizon with receding road and mountain silhouettes | epic | `#0d0015` `#1a0033` `#ff2975` `#f222ff` | stars | racing, action, sports |
---
### Indoor Theme
Interior environments and spaces.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Laboratory | `indoor_laboratory` | Dark science lab with glowing equipment and bubbling vials | mysterious | `#1a1e2e` `#2a3040` `#0f6b5e` `#44eebb` | bubbles | puzzle, trivia, rpg |
| Classroom | `indoor_classroom` | Bright classroom with chalkboard, desks, and warm sunlight | cheerful | `#f5f0e0` `#d4c9a8` `#5b8c5a` `#8b5e3c` | dust | trivia, puzzle, story |
| Bedroom | `indoor_bedroom` | Cozy bedroom at night with warm lamp glow and string lights | cozy | `#2a1f3d` `#3d2b5a` `#e8a87c` `#ffd166` | fireflies | story, creative, pet |
| Kitchen | `indoor_kitchen` | Bright kitchen with checkered floor and warm appliance glow | warm | `#fff8e7` `#f0d9a0` `#c97b3a` `#8b4513` | dust | creative, pet, puzzle |
| Arcade | `indoor_arcade` | Dark arcade with neon-lit cabinets and glowing screens | energetic | `#0d0d1a` `#1a0a2e` `#ff00ff` `#00ffff` | sparkles | action, racing, sports |
| Office | `indoor_office` | Modern office with monitor glow and large windows | focused | `#e8e4dc` `#c5bfb2` `#4a6fa5` `#2c3e50` | dust | puzzle, trivia, creative |
| Library | `indoor_library` | Grand library with towering bookshelves and reading lamps | serene | `#2c1810` `#4a3020` `#c4956a` `#f4e4c1` | dust | story, trivia, puzzle, rpg |
| Gym | `indoor_gym` | Modern gym with equipment silhouettes and bright overhead lights | energetic | `#1a1a24` `#2a2a3a` `#ff4444` `#ff8800` | dust | action, sports, racing |
---
### Weather Theme
Atmospheric and meteorological conditions.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Rain | `weather_rain` | Overcast sky with steady rainfall, puddles, and muted city outlines | melancholy | `#3a4a5a` `#2a3a4a` `#5a6a7a` `#8899aa` | rain | story, puzzle, rpg |
| Snow | `weather_snow` | Gentle snowfall over a soft winter landscape with frosted trees | peaceful | `#c8d8e8` `#a0b8cc` `#e8eef4` `#FFFFFF` | snow | puzzle, pet, creative, story |
| Storm | `weather_storm` | Dark storm clouds with heavy rain, wind-bent trees, and thunder | intense | `#1a1a25` `#252535` `#3a3a55` `#667788` | rain | action, rpg, racing |
| Sunset | `weather_sunset` | Vibrant sunset with orange-purple gradient and silhouette landscape | warm | `#1a0a2e` `#cc4400` `#ff8833` `#ffd166` | sparkles | story, creative, pet, music |
| Sunrise | `weather_sunrise` | Early morning sunrise with pink-orange sky, mist, and dewy fields | hopeful | `#2a1a40` `#884466` `#ee8855` `#ffe4a0` | dust | puzzle, pet, creative, sports |
| Fog | `weather_fog` | Thick fog with barely visible shapes and dim lights | eerie | `#8a8a8a` `#666666` `#aaaaaa` `#cccccc` | fog | story, rpg, puzzle |
| Lightning | `weather_lightning` | Pitch-dark sky lit by dramatic lightning bolts | dramatic | `#0a0a18` `#1a1a30` `#3344aa` `#eeeeff` | rain | action, rpg, racing |
| Aurora | `weather_aurora` | Dark arctic sky with shimmering aurora borealis | magical | `#0a0a1a` `#0d1b2a` `#22cc88` `#8844dd` | stars | creative, rpg, story, music |
---
### Sports Theme
Athletic venues and environments.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Stadium | `sports_stadium` | Large outdoor stadium with green pitch and bright floodlights | exciting | `#1a3a1a` `#2a5a2a` `#88cc44` `#ffffff` | sparkles | sports, action, trivia |
| Race Track | `sports_raceTrack` | Asphalt race track with lane markings and grandstand | fast | `#333333` `#555555` `#cc0000` `#ffffff` | dust | racing, action, sports |
| Court | `sports_court` | Indoor basketball court with polished hardwood and arena lighting | competitive | `#c47a34` `#e8944a` `#ffffff` `#2255aa` | sparkles | sports, action, trivia |
| Field | `sports_field` | Open soccer field under blue sky with goals and corner flags | fresh | `#2a7a2a` `#3a9a3a` `#ffffff` `#88cc44` | leaves | sports, action, pet |
| Arena | `sports_arena` | Dark fighting arena with dramatic spotlights and ropes | intense | `#1a1a2e` `#2a2a44` `#ff4444` `#ffaa00` | dust | action, sports, rpg |
| Pool | `sports_pool` | Olympic swimming pool with lane dividers and rippling water | calm | `#1a6088` `#2288aa` `#44bbdd` `#88eeff` | sparkles | sports, racing, puzzle |
| Gym | `sports_gym` | Gymnastics arena with spring floor and apparatus silhouettes | competitive | `#ddc8a0` `#c4a870` `#ffffff` `#cc3333` | dust | sports, action, creative |
| Skatepark | `sports_skatepark` | Urban skatepark with concrete ramps and graffiti | cool | `#555566` `#777788` `#ff6600` `#ffcc00` | leaves | action, sports, racing, creative |
---
### Seasonal Theme
Time-of-year and holiday-themed environments.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Spring Meadow | `seasonal_springMeadow` | A sunny spring meadow with wildflowers and gentle hills | cheerful | `#87CEEB` `#98FB98` `#FFD700` `#FF69B4` | leaves | puzzle, creative, pet |
| Autumn Leaves | `seasonal_autumnLeaves` | A warm autumn landscape with falling leaves and golden light | warm | `#2C1810` `#CC5500` `#DD8833` `#FFD700` | leaves | puzzle, story, rpg |
| Winter Wonderland | `seasonal_winterWonderland` | A serene winter landscape with snow-covered hills | calm | `#1A2744` `#4A7CB5` `#C8E0F4` `#FFFFFF` | snow | puzzle, story, creative |
| Summer Beach | `seasonal_summerBeach` | A vibrant summer beach scene with rolling waves and warm sand | energetic | `#0077BE` `#00BFFF` `#F4D03F` `#FF6347` | sparkles | action, sports, racing |
| Harvest | `seasonal_harvest` | Golden harvest fields with wheat rows and warm sunset | warm | `#5C3317` `#D4A017` `#8B4513` `#FFD700` | dust | puzzle, story, creative |
| Blossom | `seasonal_blossom` | Gentle cherry blossom petals drifting under pink-tinted skies | peaceful | `#FFB7C5` `#FF69B4` `#FFF0F5` `#98FB98` | leaves | puzzle, story, creative, music |
| Snowfall | `seasonal_snowfall` | Heavy snowfall over a dark winter night with distant lights | calm | `#0D1B2A` `#1B2838` `#415A77` `#E0E1DD` | snow | puzzle, story, rpg |
| Tropical | `seasonal_tropical` | A lush tropical scene with dense foliage and vivid colors | energetic | `#00CED1` `#FF6347` `#228B22` `#FFD700` | fireflies | action, racing, sports, pet |
---
### Underwater Theme
Aquatic and marine environments.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Coral Reef | `underwater_coralReef` | A vibrant coral reef teeming with colorful marine life | cheerful | `#006994` `#00A8CC` `#FF6F61` `#FFB347` | bubbles | puzzle, pet, creative |
| Deep Sea | `underwater_deepSea` | The mysterious deep sea with bioluminescent creatures | mysterious | `#0A0A2E` `#0D1B3E` `#1A3A5C` `#00FFCC` | fireflies | rpg, story, puzzle |
| Kelp Forest | `underwater_kelpForest` | A towering kelp forest with dappled light filtering through | peaceful | `#0B4F3A` `#1A7A5C` `#2FA87A` `#7EC8A0` | bubbles | puzzle, creative, story |
| Submarine | `underwater_submarine` | A submarine expedition through dark ocean depths | adventurous | `#0A1628` `#1A3050` `#D4AA44` `#CC4444` | bubbles | action, rpg, story |
| Shipwreck | `underwater_shipwreck` | A sunken ship resting on the ocean floor, overgrown with sea life | mysterious | `#0A1A2A` `#1A3344` `#8B7355` `#44AA88` | bubbles | rpg, story, puzzle |
| Abyss | `underwater_abyss` | The abyssal zone with volcanic vents and glowing magma cracks | dark | `#020208` `#050515` `#0A0A30` `#FF2200` | particles | action, rpg, story |
| Ice Cave | `underwater_iceCave` | An underwater ice cavern with crystalline formations | calm | `#0A2A4A` `#1A5A8A` `#88CCEE` `#E0F0FF` | sparkles | puzzle, story, creative |
| Treasure | `underwater_treasure` | A hidden underwater treasure trove glowing with golden riches | adventurous | `#0D2B45` `#1A4A6A` `#FFD700` `#FF8C00` | sparkles | rpg, action, puzzle |
---
### Sky Theme
Aerial and atmospheric perspectives.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Clouds | `sky_clouds` | A bright blue sky filled with drifting puffy white clouds | peaceful | `#4A90D9` `#87CEEB` `#FFFFFF` `#F0F8FF` | dust | puzzle, creative, pet |
| Flying High | `sky_flyingHigh` | High altitude perspective with streaking wind lines and brilliant sun | energetic | `#1E3A6E` `#4682B4` `#FFD700` `#FFFFFF` | sparkles | action, racing, sports |
| Hot Air Balloon | `sky_hotAirBalloon` | Colorful hot air balloons floating gently across a warm sky | cheerful | `#87CEEB` `#FF6347` `#FFD700` `#FFFFFF` | dust | puzzle, creative, pet, music |
| Airplane View | `sky_airplaneView` | View from an airplane window looking down at a sea of clouds | calm | `#1A3366` `#336699` `#FFFFFF` `#AACCEE` | dust | puzzle, story, creative |
| Stratosphere | `sky_stratosphere` | The edge of space where the atmosphere fades and Earth curves below | mysterious | `#0A0A2E` `#1A1A5E` `#4444AA` `#8888DD` | stars | story, rpg, puzzle |
| Bird's Eye | `sky_birdsEye` | A bird's eye view looking down at patchwork fields and winding rivers | peaceful | `#5588BB` `#88BBDD` `#228B22` `#DEB887` | dust | puzzle, creative, story |
| Horizon | `sky_horizon` | A dramatic sunset horizon with layers of warm orange, pink, and gold | warm | `#1A0533` `#CC4488` `#FF8844` `#FFD700` | dust | story, music, creative, rpg |
| Starfield | `sky_starfield` | A clear night sky bursting with stars and a faint galactic band | calm | `#000011` `#0A0A2A` `#1A1A44` `#FFFFFF` | stars | puzzle, story, rpg, music |
---
### Mechanical Theme
Industrial and technological environments.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Gears | `mechanical_gears` | Interlocking gears turning against a dark steel backdrop | focused | `#1A1A24` `#2A2A38` `#7A7A88` `#C08040` | dust | puzzle, physics, rpg |
| Circuits | `mechanical_circuits` | Circuit board traces with glowing data paths | focused | `#0A0F14` `#112222` `#00CC88` `#33FFAA` | sparkles | puzzle, trivia, physics |
| Factory | `mechanical_factory` | Factory floor with smokestacks and warning stripes | busy | `#1A1510` `#2A2520` `#8B7355` `#DDAA44` | dust | action, physics, puzzle |
| Robot Factory | `mechanical_robotFactory` | Robot assembly line with mechanical arms and sparks | playful | `#151520` `#222238` `#5588CC` `#FFAA33` | sparkles | action, puzzle, creative |
| Conveyor Belts | `mechanical_conveyorBelts` | Layered conveyor belts moving crates and parts | busy | `#18181F` `#28283A` `#666680` `#DD8822` | dust | puzzle, action, physics |
| Pipes | `mechanical_pipes` | Interconnected pipe network with valves and joints | complex | `#121218` `#1E2A1E` `#558855` `#88BB44` | dust | puzzle, physics, rpg |
| Steampunk | `mechanical_steamPunk` | Brass gears, gauges, and steam vents in warm tones | adventurous | `#1A1008` `#2A1A0A` `#C08040` `#E8C060` | dust | rpg, story, action |
| Tech Lab | `mechanical_techLab` | Clean tech laboratory with holographic displays | curious | `#0E0E1A` `#1A1A30` `#4455AA` `#66DDFF` | sparkles | puzzle, trivia, creative |
---
### Spooky Theme
Eerie and horror-themed environments.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Haunted House | `spooky_hauntedHouse` | A looming haunted house silhouette against a moonlit sky | eerie | `#0A0812` `#1A1025` `#3A2255` `#CCCC88` | fog | story, puzzle, rpg |
| Graveyard | `spooky_graveyard` | Moonlit graveyard with crooked tombstones and mist | somber | `#080810` `#121828` `#2A3348` `#99AA77` | fog | rpg, story, puzzle |
| Dark Forest | `spooky_darkForest` | Dense dark forest with twisted trees and glowing eyes | foreboding | `#050808` `#0A1510` `#1A2A18` `#3A5530` | fireflies | rpg, action, story |
| Abandoned Building | `spooky_abandonedBuilding` | Crumbling abandoned building with broken windows and debris | desolate | `#0A0A0E` `#181820` `#3A3A48` `#667766` | dust | action, puzzle, rpg |
| Crypt | `spooky_crypt` | Underground stone crypt with torchlight and coffins | dread | `#060608` `#101015` `#2A2A35` `#558855` | dust | rpg, action, story |
| Manor | `spooky_manor` | Gothic manor interior with candlelight and portraits | gothic | `#0A080C` `#1A1020` `#3A2040` `#886644` | dust | story, rpg, puzzle |
| Fog | `spooky_fog` | Thick rolling fog obscuring dark shapes and dim lights | mysterious | `#0A0E12` `#151E28` `#2A3844` `#556688` | fog | story, puzzle, rpg |
| Shadows | `spooky_shadows` | Near-total darkness with shifting shadows and faint light | paranoid | `#050506` `#0E0E14` `#1C1C2A` `#332244` | dust | action, puzzle, rpg |
---
### Colorful Theme
Vibrant and visually striking environments.
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|---------|-----|-------------|------|--------|-----------|-----------------|
| Rainbow | `colorful_rainbow` | Bold rainbow arcs and bands across a bright sky | joyful | `#FF4444` `#FFAA33` `#FFEE33` `#44DD55` | sparkles | creative, puzzle, music |
| Paint Splatter | `colorful_paintSplatter` | Colorful paint splatters on a light canvas | creative | `#FAFAE8` `#FF3366` `#3388FF` `#FFCC00` | particles | creative, music, action |
| Candy World | `colorful_candyWorld` | Candy landscape with lollipops, gumdrops, and sugar hills | sweet | `#FFE0F0` `#FF88BB` `#88DDFF` `#AAFFAA` | sparkles | pet, creative, puzzle |
| Disco | `colorful_disco` | Disco floor with mirror ball and sweeping colored lights | energetic | `#1A0A2E` `#2A1A44` `#FF33AA` `#33FFCC` | sparkles | music, action, sports |
| Neon | `colorful_neon` | Neon signs and glowing outlines against dark city night | electric | `#0A0A18` `#151530` `#FF0088` `#00FFCC` | particles | action, racing, music |
| Tie-Dye | `colorful_tieDye` | Swirling tie-dye patterns with organic color blending | psychedelic | `#FF4488` `#FFAA22` `#44DDBB` `#8844FF` | particles | creative, music, story |
| Gradient | `colorful_gradient` | Smooth shifting color gradients with geometric accents | peaceful | `#FF6B6B` `#FECA57` `#48DBFB` `#FF9FF3` | sparkles | puzzle, creative, trivia |
| Prism | `colorful_prism` | Light splitting through prisms into rainbow beams | mystical | `#1A1A2E` `#0F3460` `#E94560` `#FFFFFF` | sparkles | puzzle, physics, creative |
---
## Compatible Music Moods
Backgrounds are tagged with compatible music moods for cohesive game experiences:
| Music Mood | Example Backgrounds |
|------------|---------------------|
| ambient | space_nebula, nature_ocean, weather_fog |
| calm | nature_meadow, weather_snow, sky_clouds |
| dreamy | abstract_gradients, seasonal_blossom, weather_aurora |
| energetic | space_warpSpeed, sports_stadium, colorful_disco |
| epic | fantasy_castle, weather_sunset, mechanical_steamPunk |
| electronic | abstract_particleFields, retro_synthwave, colorful_neon |
| playful | colorful_candyWorld, retro_pixelGrid, mechanical_robotFactory |
| tense | space_asteroidField, spooky_crypt, weather_storm |
| mysterious | underwater_deepSea, spooky_fog, sky_stratosphere |
| happy | seasonal_springMeadow, sky_hotAirBalloon, underwater_treasure |
---
## Compatible Avatar Contexts
Recommended avatar context pairings for backgrounds:
| Background Theme | Recommended Avatar Contexts |
|------------------|----------------------------|
| space | astronaut, robot, alien, superhero |
| nature | explorer, hiker, animal, fairy |
| urban | cyclist, skater, runner, casual |
| fantasy | knight, wizard, fairy, dragon |
| abstract | geometric, minimal, gamer |
| retro | pixel, arcade, classic |
| indoor | student, worker, gamer, casual |
| weather | raincoat, winter, summer |
| sports | athlete, racer, swimmer |
| seasonal | holiday, themed |
| underwater | diver, mermaid, fish |
| sky | pilot, bird, balloon |
| mechanical | robot, engineer, steampunk |
| spooky | ghost, monster, vampire |
| colorful | party, disco, rainbow |
---
## API Usage
### Get All Themes
```javascript
const summary = BackgroundEngine.getThemeSummary();
// Returns: [{ theme: 'space', variants: [...], count: 8 }, ...]
```
### Get Total Background Count
```javascript
const total = BackgroundEngine.getTotalCount();
// Returns: 120
```
### Find Backgrounds by Game Genre
```javascript
const puzzleBackgrounds = BackgroundEngine.findByGenre('puzzle');
// Returns array of backgrounds recommended for puzzle games
```
### Find by Music Mood
```javascript
const calmBackgrounds = BackgroundEngine.findByMusicMood('calm');
// Returns backgrounds compatible with calm music
```
### Get Random Background from Theme
```javascript
const bg = BackgroundEngine.getRandomFromTheme('space');
// Returns: { theme: 'space', variant: 'nebula' } (random)
```
### Get Random Matching Filters
```javascript
const bg = BackgroundEngine.getRandomMatching({
mood: 'energetic',
tags: ['neon']
});
```
### Validate Catalog Integrity
```javascript
const issues = BackgroundEngine.validateCatalog();
// Returns array of any missing/invalid registrations
```
---
## Visual Preview Guide
Each background can be previewed using the BackgroundEngine:
```javascript
// Initialize canvas
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
const ctx = canvas.getContext('2d');
// Render a specific background
BackgroundEngine.set('space', 'nebula');
// In animation loop:
function render(time) {
BackgroundEngine.render(ctx, canvas.width, canvas.height, time / 1000);
requestAnimationFrame(render);
}
render(0);
```
### Color Palette Structure
Each background defines 4 colors:
- **Color 1**: Primary/dominant background color
- **Color 2**: Secondary background color
- **Color 3**: Accent/highlight color
- **Color 4**: Detail/contrast color
### Render Layers
1. **Base Layer** (`renderBase`) - Sky, ground, gradients
2. **Mid Layer** (`renderMid`) - Objects, details, animations
3. **Particle Layer** - Ambient particle effects (optional)
---
## Catalog Statistics
| Metric | Value |
|--------|-------|
| Total Themes | 15 |
| Variants Per Theme | 8 |
| Total Backgrounds | 120 |
| Particle Types | 10 |
| Moods Available | 16 |
| Game Genres Mapped | 10 |
---
*Last Updated: 2026-02-05*
*Version: 1.0.0*
+721
View File
@@ -0,0 +1,721 @@
# Context Catalog
## Overview
26 game contexts that place avatars in themed scenarios.
3 categories: Vehicles (12), Costumes (10), Pure (4).
Each context defines:
- **Avatar Mode**: How much of the avatar is visible (HEAD_ONLY, HEAD_AND_SHOULDERS, UPPER_BODY, FULL_BODY)
- **Animations**: Available animation states for the avatar
- **Accessory Visibility**: Which avatar accessories (eyewear, headwear, effects) are visible
- **Effects**: Visual effects triggered by game states
- **Recommended For**: Game types that work well with this context
---
## Vehicle Contexts (12)
Vehicles place the avatar inside cockpits, vehicles, or as part of a flying/riding scenario.
---
### spaceship-cockpit
- **ID**: `spaceship-cockpit`
- **Mode**: HEAD_ONLY
- **Description**: Pilot a spacecraft through space
- **Background**: Dark space with animated starfield, cockpit interior frame with side panels
- **Foreground**: Cockpit glass with blue tint, HUD targeting reticle, control panel with blinking buttons, speed indicator
- **Animations**: idle, bob, celebrate, hurt
- **Effects**:
- `boost`: Particle flames from rear
- `damage`: Screen shake (intensity 5)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Space shooters, sci-fi games, arcade
- **Recommended Music**: Epic/Intense
- **Recommended Backgrounds**: Space, nebula, asteroid fields
---
### race-car
- **ID**: `race-car`
- **Mode**: HEAD_AND_SHOULDERS
- **Description**: Race at high speed on the track
- **Background**: Sky gradient, road rushing past with animated lane markers, speed lines on sides
- **Foreground**: Dashboard with curve, steering wheel, side mirrors with road reflections, speedometer and RPM gauges with animated needles
- **Animations**: idle, bob, celebrate, hurt, turn-left, turn-right
- **Effects**:
- `accelerate`: Vertical blur
- `drift`: Side particle effects
- `damage`: Screen shake (intensity 8)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Racing games, driving simulations, arcade racers
- **Recommended Music**: Intense, electronic
- **Recommended Backgrounds**: Urban, track, highway
---
### airplane
- **ID**: `airplane`
- **Mode**: HEAD_AND_SHOULDERS
- **Description**: Fly through the clouds as a pilot
- **Background**: Sky gradient (blue), animated clouds streaming past, cockpit interior sides
- **Foreground**: Windshield frame with center divider, instrument panel with 5 gauges (animated needles), yoke/control column, altitude and heading displays
- **Animations**: idle, bob, celebrate, hurt
- **Effects**:
- `turbulence`: Screen shake (intensity 3)
- `dive`: Vertical blur
- `damage`: Screen shake (intensity 10)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Flight simulators, arcade flyers
- **Recommended Music**: Adventurous, orchestral
- **Recommended Backgrounds**: Clouds, sky, mountains
---
### flappy-style
- **ID**: `flappy-style`
- **Mode**: HEAD_ONLY
- **Description**: Flap through obstacles as a flying character
- **Background**: Transparent (game provides its own)
- **Foreground**: Animated wing sprites on left and right sides with flapping motion, wing detail lines
- **Animations**: idle, flap, fall, hurt
- **Effects**:
- `flap`: Vertical blur (intensity 2)
- `fall`: Downward stretch
- `damage`: Red flash
- **Accessory Visibility**: Eyewear visible, Headwear hidden, Effects visible
- **Best For**: Flappy-style games, casual mobile games, arcade
- **Recommended Music**: Playful, casual
- **Recommended Backgrounds**: Sky, pipes, obstacles
---
### tank
- **ID**: `tank`
- **Mode**: HEAD_ONLY
- **Description**: Command a battle tank
- **Background**: Military green interior with radial gradient, riveted panel details, side screens with static effect
- **Foreground**: Circular tank hatch frame with bolts, periscope view with crosshair at top, control panel with warning lights, ammo counter display
- **Animations**: idle, bob, celebrate, hurt, aim
- **Effects**:
- `fire`: Yellow flash (intensity 10)
- `rumble`: Screen shake (intensity 4)
- `damage`: Screen shake (intensity 12)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Military shooters, strategy games, arcade combat
- **Recommended Music**: Intense, military
- **Recommended Backgrounds**: Battlefield, desert, urban warfare
---
### pacman-style
- **ID**: `pacman-style`
- **Mode**: HEAD_ONLY
- **Description**: Navigate mazes and eat pellets
- **Background**: Transparent (game provides maze)
- **Foreground**: Subtle mouth/chomp overlay effect
- **Animations**: idle, chomp, hurt, power
- **Effects**:
- `chomp`: Scale pulse (rate 12)
- `power`: Cyan glow
- `damage`: Red flash
- **Accessory Visibility**: Eyewear hidden, Headwear hidden, Effects visible
- **Best For**: Maze games, arcade classics, retro games
- **Recommended Music**: Retro, chiptune
- **Recommended Backgrounds**: Maze walls, pellets
---
### hoverboard
- **ID**: `hoverboard`
- **Mode**: UPPER_BODY
- **Description**: Ride a futuristic hoverboard
- **Background**: Ground rushing beneath with speed lines, shadow beneath board
- **Foreground**: Animated hoverboard with hover glow effect, board body with gradient, cyan lights, hover jets with flicker
- **Animations**: idle, bob, celebrate, hurt, lean-left, lean-right, jump
- **Effects**:
- `idle`: Bob motion (amplitude 3, rate 4)
- `boost`: Cyan particle trail behind
- `jump`: Upward stretch
- `damage`: Screen shake (intensity 6)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Endless runners, racing games, arcade
- **Recommended Music**: Electronic, futuristic
- **Recommended Backgrounds**: City streets, futuristic
---
### jetpack
- **ID**: `jetpack`
- **Mode**: UPPER_BODY
- **Description**: Fly with a jetpack strapped to your back
- **Background**: Sky gradient, clouds below, jetpack unit behind avatar (main body, fuel gauge, exhaust nozzles)
- **Foreground**: Animated jet flames with flicker, smoke trail particles
- **Animations**: idle, bob, celebrate, hurt, boost
- **Effects**:
- `idle`: Bob motion (amplitude 2, rate 2)
- `boost`: Orange particle trail downward
- `damage`: Screen shake (intensity 8)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Flying games, endless flyers, arcade shooters
- **Recommended Music**: Action, adventure
- **Recommended Backgrounds**: Sky, clouds, obstacles
---
### mech-suit
- **ID**: `mech-suit`
- **Mode**: HEAD_ONLY
- **Description**: Pilot a powerful mech suit
- **Background**: Mech interior with radial gradient, side screens (radar with sweep, system status), interior frame elements
- **Foreground**: Hexagonal chest window frame, HUD targeting brackets, control panel with joysticks, warning lights (blinking), status text
- **Animations**: idle, bob, celebrate, hurt
- **Effects**:
- `walk`: Screen shake (intensity 2)
- `fire`: Yellow flash
- `damage`: Screen shake (intensity 10)
- **Accessory Visibility**: Eyewear hidden, Headwear hidden, Effects visible
- **Best For**: Mech combat, shooters, action games
- **Recommended Music**: Epic, industrial
- **Recommended Backgrounds**: City destruction, battlefield
---
### submarine
- **ID**: `submarine`
- **Mode**: HEAD_AND_SHOULDERS
- **Description**: Explore the depths in a submarine
- **Background**: Underwater gradient (dark blue), rising bubbles with wobble animation, submarine interior with pipes and joints
- **Foreground**: Circular porthole frame with rivets, glass reflection, water distortion overlay, control panel with depth gauge, pressure gauge, sonar ping effect, periscope controls
- **Animations**: idle, bob, celebrate, hurt
- **Effects**:
- `idle`: Bob motion (amplitude 2, rate 1)
- `pressure`: Distortion effect (intensity 3)
- `damage`: Screen shake (intensity 6)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Underwater exploration, arcade games
- **Recommended Music**: Ambient, mysterious
- **Recommended Backgrounds**: Ocean depths, coral, sea creatures
---
### dragon-rider
- **ID**: `dragon-rider`
- **Mode**: FULL_BODY
- **Description**: Soar through the skies on a dragon
- **Background**: Sunset sky gradient, clouds, dragon neck with scales, animated wings (flapping), saddle beneath rider
- **Foreground**: Dragon horns and ears framing view, dragon body/tail below, dragon legs with claws
- **Animations**: idle, bob, celebrate, hurt, lean
- **Effects**:
- `idle`: Bob motion (amplitude 5, rate 2)
- `fireBreath`: Orange glow
- `dive`: Vertical blur
- `damage`: Screen shake (intensity 8)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Fantasy adventures, flying games, RPGs
- **Recommended Music**: Epic, fantasy orchestral
- **Recommended Backgrounds**: Mountains, castles, sky
---
### motorcycle
- **ID**: `motorcycle`
- **Mode**: FULL_BODY
- **Description**: Race on a high-speed motorcycle
- **Background**: Sunset sky, road with animated lane markers, environment blur on sides, motion blur lines
- **Foreground**: Full motorcycle with body, fuel tank with stripe, engine with details, exhaust pipes, front and rear wheels with spinning spokes, handlebars, mirrors, headlight, seat, wheel speed blur
- **Animations**: idle, bob, celebrate, hurt, lean-left, lean-right
- **Effects**:
- `accelerate`: Vertical blur
- `wheelie`: Rotation (-15 degrees)
- `lean`: Rotation (20 degrees)
- `damage`: Screen shake (intensity 8)
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Racing games, endless runners, arcade action
- **Recommended Music**: Rock, intense
- **Recommended Backgrounds**: Highway, city streets
---
## Costume Contexts (10)
Costumes dress the avatar in themed outfits with props and accessories.
---
### knight-armor
- **ID**: `knight-armor`
- **Mode**: FULL_BODY
- **Description**: Medieval knight in shining armor
- **Costume Details**:
- **Under Layer**: Flowing red cape with gold trim, silver chestplate with ridge details, leg armor (greaves) with knee guards, armored boots
- **Over Layer**: Shoulder pauldrons with spikes, helmet with visor (eye slits), red helmet crest
- **Props**:
- Sword (right hand): Silver blade, gold guard, leather handle
- Shield (left arm): Silver with red emblem and gold cross
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `attack`: Silver slash
- `hurt`: Orange sparks
- **Accessory Visibility**: Eyewear hidden (behind visor), Headwear visible (above helmet), Effects visible
- **Best For**: RPGs, medieval games, action adventures
- **Recommended Music**: Epic, heroic
---
### space-suit
- **ID**: `space-suit`
- **Mode**: FULL_BODY
- **Description**: Astronaut ready for space exploration
- **Costume Details**:
- **Under Layer**: White suit body with blue accent stripes, oxygen pack on back with blinking lights, legs with stripes, magnetic boots
- **Over Layer**: Transparent helmet bubble with reflection, helmet ring, blue accent dome, antenna with red tip, white gloves with blue centers
- **Props**: None
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `hurt`: Cyan sparks
- **Accessory Visibility**: Eyewear visible (through helmet), Headwear visible (above helmet), Effects visible
- **Best For**: Action games, adventure, physics puzzles
- **Recommended Music**: Ambient, electronic
---
### ninja-outfit
- **ID**: `ninja-outfit`
- **Mode**: FULL_BODY
- **Description**: Stealthy ninja warrior
- **Costume Details**:
- **Under Layer**: Black body suit, katana on back (scabbard with red handle wrap, gold guard), gray belt with throwing stars, black leg wraps, tabi boots (split-toe)
- **Over Layer**: Black arm wraps, face mask covering lower face, red headband with flowing tail (animated)
- **Props**:
- Katana (on back)
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `attack`: Dark slash
- `hurt`: Gray smoke
- **Accessory Visibility**: Eyewear visible (through eye slit), Headwear hidden (mask covers), Effects visible
- **Best For**: Action games, stealth adventures, RPGs
- **Recommended Music**: Tense, mysterious
---
### wizard-robes
- **ID**: `wizard-robes`
- **Mode**: FULL_BODY
- **Description**: Mystical wizard with flowing robes
- **Costume Details**:
- **Under Layer**: Dark blue flowing robe (animated), inner fold, leather belt with gold buckle, side pouches, glowing purple runes on fabric (pulsing)
- **Over Layer**: Wide sleeves, hood/collar, staff in right hand
- **Props**:
- Staff (right hand): Brown wooden shaft, purple orb with glow effect
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `attack`: Purple magic burst
- `celebrate`: Gold sparkles
- **Accessory Visibility**: Eyewear visible, Headwear visible (optional wizard hat), Effects visible
- **Best For**: RPGs, puzzle games, adventures
- **Recommended Music**: Mystical, fantasy
---
### superhero-cape
- **ID**: `superhero-cape`
- **Mode**: FULL_BODY
- **Description**: Classic superhero with billowing cape
- **Costume Details**:
- **Under Layer**: Billowing cape (animated with waves), bodysuit in avatar's clothing color, gold belt with white buckle, colored trunks, legs in suit color, colored boots with gold tops
- **Over Layer**: Suit arms, colored gloves with gold cuffs, gold chest emblem (star shape), colored mask with white eye holes
- **Props**: None
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `attack`: Yellow impact burst
- `celebrate`: Gold sparkles
- **Accessory Visibility**: Eyewear visible (as mask), Headwear visible, Effects visible
- **Best For**: Action games, adventure
- **Recommended Music**: Heroic, triumphant
---
### athlete-uniform
- **ID**: `athlete-uniform`
- **Mode**: FULL_BODY
- **Description**: Athletic sports uniform with number 23
- **Costume Details**:
- **Under Layer**: Red jersey with white stripes, number 23 on back, dark shorts with red side stripes, exposed legs, white socks with red stripes, black athletic shoes with red accents
- **Over Layer**: Red sleeves with white stripes, red headband, white wristbands, small number 23 on front
- **Props**:
- Ball (optional, right arm)
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `celebrate`: Gold confetti
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
- **Best For**: Sports games, action, racing
- **Recommended Music**: Energetic, pump-up
---
### pirate-outfit
- **ID**: `pirate-outfit`
- **Mode**: FULL_BODY
- **Description**: Swashbuckling pirate captain
- **Costume Details**:
- **Under Layer**: Dark red long coat (animated back), white ruffled shirt, leather belt with gold buckle, dark pants, brown boots with folded tops
- **Over Layer**: Coat front panels with gold trim and buttons, cutlass in right hand (curved blade, gold guard, leather handle), black tricorn hat with gold trim and skull emblem
- **Props**:
- Cutlass (right hand)
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `attack`: Silver slash
- **Accessory Visibility**: Eyewear visible (eyepatch optional), Headwear visible, Effects visible
- **Best For**: Adventure games, action, RPGs
- **Recommended Music**: Adventurous, sea shanty
---
### scientist-labcoat
- **ID**: `scientist-labcoat`
- **Mode**: FULL_BODY
- **Description**: Mad scientist ready to experiment
- **Costume Details**:
- **Under Layer**: Blue shirt with dark tie, dark gray pants, brown sensible shoes
- **Over Layer**: White lab coat (open front with collar), sleeves, pocket protector with colored pens (blue, red, black), ID badge with photo, safety goggles on forehead
- **Props**:
- Beaker (right hand): Glass container with bubbling green liquid and animated bubbles
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**:
- `attack`: Green bubbles
- **Accessory Visibility**: Eyewear visible (can wear both goggles and glasses), Headwear visible, Effects visible
- **Best For**: Puzzle games, physics games, creative games
- **Recommended Music**: Quirky, experimental
---
### chef-outfit
- **ID**: `chef-outfit`
- **Mode**: FULL_BODY
- **Description**: Master chef ready to cook
- **Costume Details**:
- **Under Layer**: White double-breasted chef jacket with buttons, white apron with strings and pocket, dark pants, black shoes
- **Over Layer**: White sleeves, red kitchen towel over shoulder with white stripes, tall white chef hat (toque) with puffy top, utensils in apron pocket
- **Props**:
- Spatula (right hand): Brown handle, metal slotted head
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**: None
- **Accessory Visibility**: Eyewear visible, Headwear visible (under chef hat), Effects visible
- **Best For**: Cooking games, creative games, puzzle
- **Recommended Music**: Upbeat, cheerful
---
### cowboy-gear
- **ID**: `cowboy-gear`
- **Mode**: FULL_BODY
- **Description**: Wild west cowboy
- **Costume Details**:
- **Under Layer**: Tan shirt with pointed collar, brown vest, leather belt with large gold oval buckle, blue jeans with seam details, brown cowboy boots with heels and silver spurs
- **Over Layer**: Tan sleeves, red bandana around neck with knot and animated tails, brown cowboy hat with curved brim and gold buckle on band
- **Props**:
- Lasso (right hand, optional)
- **Animations**: idle, walk, attack, hurt, celebrate
- **Effects**: None
- **Accessory Visibility**: Eyewear visible, Headwear visible (under hat), Effects visible
- **Best For**: Western action, adventure games
- **Recommended Music**: Western, country
---
## Pure Contexts (4)
Pure contexts apply minimal modifications to the avatar, using only effects and subtle accessories. Avatar is fully visible in FULL_BODY mode.
---
### platformer-standard
- **ID**: `platformer-standard`
- **Mode**: FULL_BODY
- **Description**: Standard platforming character with no costume modifications
- **Modifications**: Dynamic shadow, dust effects only
- **Visual Effects**:
- Shadow beneath avatar (scales with elevation)
- Launch dust when jumping starts
- Landing impact dust when touching ground
- Running dust puffs
- Circling stars when hurt
- Celebratory confetti
- **Animations**: idle, walk, run, jump, fall, crouch, hurt, celebrate
- **State Effects**:
- `running`: Dust puffs (frequency 0.1)
- `jumping`: Launch dust (once)
- `landing`: Impact dust (once)
- `hurt`: Stars around head (duration 0.5s)
- `celebrating`: Confetti (duration 3.0s)
- **Accessory Visibility**: All visible
- **Best For**: Platformers, adventure games, action games
- **Recommended Music**: Playful, heroic
---
### sports-player
- **ID**: `sports-player`
- **Mode**: FULL_BODY
- **Description**: Athletic character with minimal sports equipment (jersey number 7)
- **Modifications**: Sweatband on forehead, jersey number on back
- **Visual Effects**:
- Elongated court/field shadow
- Speed lines when running fast (threshold 0.7)
- Motion blur when jumping at speed
- Sparkles when scoring
- Impact flash when hurt
- **Animations**: idle, walk, run, jump, fall, crouch, hurt, celebrate, throw, catch, kick
- **State Effects**:
- `running`: Speed lines (threshold 0.7)
- `jumping`: Motion blur
- `scoring`: Sparkles (duration 1.0s)
- `hurt`: Flash (duration 0.3s)
- `celebrating`: Sparkles (duration 2.0s)
- **Accessory Visibility**: All visible
- **Best For**: Sports games, basketball, soccer, tennis, racing
- **Recommended Music**: Energetic
---
### adventure-hero
- **ID**: `adventure-hero`
- **Mode**: FULL_BODY
- **Description**: Adventurous character with backpack and utility belt
- **Modifications**: Small brown backpack on back, thin utility belt with pouches
- **Visual Effects**:
- Dynamic shadow based on light source
- Subtle heroic glow when idle (pulsing gold)
- Determination lines (golden speed lines) when running
- Cape-like motion trails when jumping (blue trailing lines)
- Discovery sparkle effect
- Damage flash when hurt
- **Animations**: idle, walk, run, jump, fall, crouch, hurt, celebrate, climb, push, pull
- **State Effects**:
- `idle`: Glow (continuous)
- `running`: Determination lines (continuous)
- `jumping`: Cape motion trails (continuous)
- `discovering`: Sparkle (duration 1.5s)
- `hurt`: Damage flash (duration 0.4s)
- **Accessory Visibility**: All visible
- **Best For**: Adventure games, puzzle games, exploration, lite RPGs
- **Recommended Music**: Adventurous
---
### runner
- **ID**: `runner`
- **Mode**: FULL_BODY
- **Description**: Streamlined character optimized for endless runner games
- **Modifications**: None (pure avatar)
- **Visual Effects**:
- Motion-blurred shadow (trails behind based on speed)
- Constant speed lines (intensity based on speed)
- Intensified speed effect when accelerating
- Air trail when jumping/falling
- Wind-swept hair effect (wind lines from head)
- Ground friction sparks when sliding
- Pickup sparkle when collecting items
- Stumble effect with shake lines when hurt
- Stars for big hits
- **Avatar Position**: Slightly left (x: 0.4) to show forward space
- **Default Animation**: run (not idle)
- **Animations**: idle, run, jump, slide, fall, hurt, celebrate
- **State Effects**:
- `running`: Speed lines (continuous)
- `accelerating`: Intensified speed (continuous)
- `jumping`: Air trail (continuous)
- `sliding`: Sparks (continuous)
- `collecting`: Sparkle (duration 0.5s)
- `hurt`: Stumble effect (duration 0.6s)
- **Accessory Visibility**: All visible
- **Best For**: Endless runners, racing, speed games
- **Recommended Music**: Fast-paced, electronic
---
## Context Selection Guide
### By Game Type
| Game Type | Recommended Contexts |
|-----------|---------------------|
| Space shooter | spaceship-cockpit, mech-suit |
| Racing | race-car, motorcycle, hoverboard |
| Flying/Flappy | flappy-style, airplane, jetpack, dragon-rider |
| Platformer | platformer-standard, adventure-hero, superhero-cape |
| Endless Runner | runner, hoverboard, motorcycle |
| RPG | knight-armor, wizard-robes, ninja-outfit, adventure-hero |
| Sports | athlete-uniform, sports-player |
| Puzzle | platformer-standard, scientist-labcoat, wizard-robes |
| Cooking | chef-outfit |
| Western | cowboy-gear |
| Underwater | submarine, space-suit |
| Fantasy | dragon-rider, knight-armor, wizard-robes |
| Arcade/Retro | pacman-style, flappy-style, spaceship-cockpit |
| Military | tank, mech-suit |
| Stealth | ninja-outfit |
| Pirate/Adventure | pirate-outfit, adventure-hero |
### By Avatar Mode
| Mode | Contexts |
|------|----------|
| HEAD_ONLY | spaceship-cockpit, tank, pacman-style, mech-suit, flappy-style |
| HEAD_AND_SHOULDERS | race-car, airplane, submarine |
| UPPER_BODY | hoverboard, jetpack |
| FULL_BODY | dragon-rider, motorcycle, all costumes (10), all pure (4) |
### By Category
| Category | Count | Contexts |
|----------|-------|----------|
| Vehicle | 12 | spaceship-cockpit, race-car, airplane, flappy-style, tank, pacman-style, hoverboard, jetpack, mech-suit, submarine, dragon-rider, motorcycle |
| Costume | 10 | knight-armor, space-suit, ninja-outfit, wizard-robes, superhero-cape, athlete-uniform, pirate-outfit, scientist-labcoat, chef-outfit, cowboy-gear |
| Pure | 4 | platformer-standard, sports-player, adventure-hero, runner |
---
## API Usage
### Getting Context Information
```javascript
// Get all contexts
const allContexts = ContextCatalog.getAll();
// Get a specific context by ID
const context = ContextCatalog.getById('spaceship-cockpit');
// Get contexts by category
const vehicles = ContextCatalog.getByCategory('vehicle');
const costumes = ContextCatalog.getByCategory('costume');
const pure = ContextCatalog.getByCategory('pure');
// Get contexts by avatar mode
const headOnly = ContextCatalog.getByMode('HEAD_ONLY');
const fullBody = ContextCatalog.getByMode('FULL_BODY');
// Get recommended contexts for a game type
const rpgContexts = ContextCatalog.getRecommendedFor('rpg');
const racingContexts = ContextCatalog.getRecommendedFor('racing');
// Search contexts
const matches = ContextCatalog.search('knight');
// Filter contexts
const filtered = ContextCatalog.filter({
category: 'costume',
mode: 'FULL_BODY',
gameType: 'rpg'
});
// Get random context
const random = ContextCatalog.getRandom();
const randomForType = ContextCatalog.getRandomForGameType('platformer');
```
### Applying Context to Avatar
```javascript
// Apply context to an avatar object
const avatarWithContext = ContextRenderer.apply(avatar, 'spaceship-cockpit');
// Render the avatar with context
ContextRenderer.render(avatarWithContext, ctx, x, y, {
scale: 1.0,
time: elapsedTime
});
// Trigger an animation
ContextRenderer.animate(avatarWithContext, 'celebrate');
// Update state for effects
ContextRenderer.setState(avatarWithContext, {
running: true,
speed: 0.8,
facingRight: true
});
```
### Context Object Structure
```javascript
{
id: 'context-id',
name: 'Human Readable Name',
category: 'vehicle' | 'costume' | 'pure',
avatarMode: 'HEAD_ONLY' | 'HEAD_AND_SHOULDERS' | 'UPPER_BODY' | 'FULL_BODY',
animations: ['idle', 'walk', 'attack', ...],
defaultAnimation: 'idle',
accessoryVisibility: {
eyewear: true | false,
headwear: true | false,
effects: true | false
},
layerOrder: ['background', 'avatar', 'foreground'],
recommendedFor: ['game-type-1', 'game-type-2'],
description: 'Short description',
avatarPosition: { x: 0.5, y: 0.5 },
avatarScale: 1.0,
props: [
{ id: 'prop-id', anchor: 'rightArm', offset: { x: 0, y: 0 } }
],
contextEffects: {
effectName: { type: 'effect-type', ... }
},
renderBackground: function(ctx, width, height, time, options) { ... },
renderForeground: function(ctx, width, height, time, options) { ... },
renderCostumeUnder: function(ctx, avatar, x, y, scale, transforms, time) { ... },
renderCostumeOver: function(ctx, avatar, x, y, scale, transforms, time) { ... }
}
```
---
## Statistics
- **Total Contexts**: 26
- **Vehicle Contexts**: 12
- **Costume Contexts**: 10
- **Pure Contexts**: 4
- **Version**: 1.0.0
- **Last Updated**: 2026-02-05
---
## Source Files
- `/frontend/public/assets/avatar/contextCatalog.js` - Main catalog aggregator
- `/frontend/public/assets/avatar/contextRenderer.js` - Rendering engine
- `/frontend/public/assets/avatar/contexts/vehicles.js` - Vehicle context definitions (12)
- `/frontend/public/assets/avatar/contexts/costumes.js` - Costume context definitions (10)
- `/frontend/public/assets/avatar/contexts/pure.js` - Pure context definitions (4)
@@ -0,0 +1,170 @@
# Game Generation Guide for AI
This guide helps Haiku (Claude AI) select appropriate assets when generating games.
## Decision Tree
### Step 1: Identify Game Style
Based on user's prompt, classify into one of 11 styles:
| Keywords | Game Style |
|----------|------------|
| shoot, space, action, fast, arcade | action-arcade |
| puzzle, match, brain, logic | puzzle |
| story, adventure, narrative, choice | story-adventure |
| race, car, speed, run | racing |
| pet, animal, care, farm | pet-simulator |
| quiz, trivia, question, knowledge | trivia-quiz |
| music, rhythm, dance, beat | music-rhythm |
| draw, create, art, design | creative-drawing |
| rpg, battle, quest, dungeon | rpg-battle |
| sports, ball, soccer, basketball | sports |
| physics, pinball, bounce, gravity | physics-pinball |
### Step 2: Detect Theme
| Theme Keywords | Background Theme |
|----------------|-----------------|
| space, alien, star, galaxy | space |
| forest, nature, tree, outdoor | nature |
| city, street, building, urban | urban |
| magic, wizard, dragon, castle | fantasy |
| ghost, haunted, scary, dark | spooky |
| ocean, fish, underwater, sea | underwater |
| retro, pixel, arcade, classic | retro |
| sports, stadium, field, court | sports |
| sky, flying, clouds, air | sky |
| rainbow, colorful, bright, party | colorful |
| robot, machine, factory, tech | mechanical |
| home, room, kitchen, office | indoor |
| rain, snow, storm, weather | weather |
| spring, summer, autumn, winter | seasonal |
| abstract, minimal, pattern | abstract |
### Step 3: Detect Mood
| Mood Keywords | Music Mood |
|---------------|------------|
| epic, battle, intense, boss | Epic |
| calm, relax, peaceful, zen | Chill |
| fast, action, rush, chase | Intense |
| fun, happy, cute, silly | Playful |
| mystery, scary, dark, creepy | Mysterious |
| hero, adventure, brave | Heroic |
| weird, unique, strange | Quirky |
| background, ambient, subtle | Ambient |
### Step 4: Determine Avatar Mode
| Game Mechanic | Avatar Mode | Context |
|---------------|-------------|---------|
| Cockpit/driving view | HEAD_ONLY | spaceship-cockpit, tank, mech-suit |
| Driving with body visible | HEAD_AND_SHOULDERS | race-car, airplane, submarine |
| Upper body action | UPPER_BODY | hoverboard, jetpack |
| Full character movement | FULL_BODY | platformer-standard, costumes |
### Step 5: Select Context
| Game Type | Recommended Context |
|-----------|---------------------|
| Space shooter | spaceship-cockpit |
| Racing game | race-car, motorcycle |
| Flappy clone | flappy-style |
| Tank game | tank |
| Maze game (pacman) | pacman-style |
| Hoverboard/skateboard | hoverboard |
| Jetpack game | jetpack |
| Mech combat | mech-suit |
| Underwater | submarine |
| Dragon game | dragon-rider |
| Platformer | platformer-standard |
| RPG/Adventure | knight-armor, wizard-robes, ninja-outfit |
| Sports | athlete-uniform, sports-player |
| Cooking | chef-outfit |
| Science | scientist-labcoat |
## Best Practices
### 1. Use Presets for Variety
```javascript
// DON'T: Always use the same combination
background: { theme: 'space', variant: 'nebula' }
// DO: Use different presets or variations
const preset = AssetPresets.getRandom('action-arcade');
```
### 2. Match Mood and Theme
```javascript
// GOOD: Epic music + Space background
{ music: { mood: 'Epic' }, background: { theme: 'space' } }
// BAD: Chill music + Intense action background
{ music: { mood: 'Chill' }, background: { theme: 'mechanical' } }
```
### 3. Match Context to Gameplay
```javascript
// GOOD: Flying game → HEAD_ONLY + flappy-style
// GOOD: Platformer → FULL_BODY + platformer-standard
// BAD: Racing game → FULL_BODY (should be HEAD_AND_SHOULDERS)
```
### 4. Validate Combinations
```javascript
const score = AssetCompatibility.validate(config).score;
// Aim for score > 70
```
## Example Selections
### "space shooter with aliens"
```javascript
{
music: { mood: 'Epic', energy: 8 },
background: { theme: 'space', variant: 'nebula' },
avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
}
```
### "relaxing puzzle game with nature"
```javascript
{
music: { mood: 'Chill', energy: 3 },
background: { theme: 'nature', variant: 'meadow' },
avatarContext: { mode: 'FULL_BODY', context: 'platformer-standard' }
}
```
### "medieval RPG with dragon battles"
```javascript
{
music: { mood: 'Epic', energy: 9 },
background: { theme: 'fantasy', variant: 'castle' },
avatarContext: { mode: 'FULL_BODY', context: 'knight-armor' }
}
```
### "scary haunted house escape"
```javascript
{
music: { mood: 'Mysterious', energy: 5 },
background: { theme: 'spooky', variant: 'hauntedHouse' },
avatarContext: { mode: 'FULL_BODY', context: 'adventure-hero' }
}
```
## Tracking Variety
To ensure games don't all look the same:
1. Rotate through presets (don't always pick index 0)
2. Use different background variants within same theme
3. Mix costumes even for similar game types
4. Vary music energy levels
## Template Selection
- Use `gameTemplateAssets.html` when avatar matters
- Use `gameTemplateSimple.html` for abstract/puzzle games without avatars
+406
View File
@@ -0,0 +1,406 @@
# Music Catalog
## Overview
160 procedural songs generated using Tone.js.
8 moods x 20 songs each.
All songs are synthesized in real-time using the MusicEngine, which reads song definitions from MusicLibrary and generates audio procedurally using Tone.js synths, effects, and chord progressions.
---
## Mood Categories
### Epic (20 songs)
Energy range: 7-10
Best for: Boss battles, climactic moments, action sequences
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| epic_001 | Siege of Thunder | 9 | 150 | C minor | action, siege, storm | Thunderous orchestral assault with relentless percussion and soaring brass lines |
| epic_002 | Dragon's Wrath | 10 | 160 | D harmonic minor | dragon, boss-fight, powerful | Ferocious harmonic minor shred with explosive drum fills and scorching lead |
| epic_003 | Kingdom's Fall | 8 | 135 | E dorian | kingdom, dramatic, war | Brooding dorian march that builds from sorrowful strings to a devastating climax |
| epic_004 | Titan Ascendant | 9 | 145 | F minor | titan, heroic, climax | Ascending power theme with massive layered synths and triumphant resolution |
| epic_005 | Forge of Legends | 8 | 130 | G melodic minor | forge, battle, powerful | Heavy melodic minor groove with hammering rhythms and a blazing lead melody |
| epic_006 | Storm Legion | 9 | 155 | A harmonic minor | storm, legion, fierce | Rapid harmonic minor fury with militant snare rolls and piercing lead runs |
| epic_007 | Conquest of Dawn | 8 | 140 | B minor | conquest, heroic, victory | Majestic dawn-rise theme with layered pads building to a grand fanfare |
| epic_008 | Blood of the Arena | 10 | 165 | C# phrygian | battle, fierce, combat | Phrygian gladiator anthem with thundering kicks and savage melodic hooks |
| epic_009 | Throne Eternal | 7 | 125 | D# dorian | throne, dramatic, kingdom | Stately dorian processional with regal brass swells and ceremonial percussion |
| epic_010 | Wings of Valkyrie | 9 | 152 | F# harmonic minor | heroic, action, victory | Soaring harmonic minor flight with rapid arpeggios and a euphoric peak |
| epic_011 | The Iron March | 8 | 132 | G# minor | war, legion, march | Crushing military march with iron-clad percussion and a resolute melodic theme |
| epic_012 | Celestial Siege | 9 | 148 | A# melodic minor | siege, climax, powerful | Heavenly melodic minor ascent colliding with earth-shaking low end and choir-like pads |
| epic_013 | Oathbreaker | 10 | 170 | E harmonic minor | boss-fight, aggressive, dramatic | Blistering boss battle anthem with relentless double-kicks and a searing harmonic minor riff |
| epic_014 | Pillars of Eternity | 7 | 120 | F dorian | dramatic, kingdom, throne | Grand dorian hymn with towering pad layers and a slow-building, inevitable crescendo |
| epic_015 | The Last Bastion | 9 | 142 | A minor | siege, heroic, battle | Desperate last-stand theme with urgent strings and a defiant, rising chorus |
| epic_016 | Warcry of the North | 10 | 158 | B harmonic minor | war, fierce, conquest | Nordic war chant with thunderous toms, harsh harmonic minor runs, and tribal energy |
| epic_017 | Shattered Crown | 8 | 138 | C# dorian | dramatic, throne, powerful | Regal dorian tragedy with broken chord progressions that rebuild into glory |
| epic_018 | Dragonfire Overture | 9 | 162 | D melodic minor | dragon, action, climax | Scorching melodic minor overture with blazing fast arpeggios and massive drops |
| epic_019 | Echoes of Valhalla | 8 | 128 | G minor | victory, heroic, legend | Heroic afterglow theme with echoing delays, reverent pads, and a noble melody |
| epic_020 | Apex Predator | 10 | 168 | D# phrygian | boss-fight, titan, aggressive | Phrygian apex boss theme with crushing low end, frantic leads, and zero mercy |
---
### Chill (20 songs)
Energy range: 1-4
Best for: Puzzle games, menus, relaxation
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| chill_001 | Sunset Horizon | 2 | 80 | C major | sunset, calm, peaceful | Warm golden-hour pads with gentle plucked melodies drifting over a soft beat |
| chill_002 | Cloud Nine | 2 | 85 | D lydian | cloud, zen, soft | Floating lydian dreamscape with airy pads and a delicate crystalline lead |
| chill_003 | Gentle Stream | 1 | 72 | F major | stream, peaceful, meditation | Babbling brook ambience with minimal plucked notes and vast reverb spaces |
| chill_004 | Amber Dusk | 3 | 90 | G mixolydian | sunset, smooth, lounge | Laid-back mixolydian groove with a silky lead line and soft shuffled percussion |
| chill_005 | Velvet Rain | 2 | 78 | A dorian | rain, calm, soft | Tender dorian rainfall with muted plucks and warm sub-bass undertones |
| chill_006 | Morning Dew | 3 | 95 | E pentatonic major | breeze, garden, easy | Fresh pentatonic morning with sparkling plucks and a gentle rhythmic pulse |
| chill_007 | Sapphire Coast | 3 | 92 | B major | shore, smooth, relax | Coastal shimmer with wave-like pad swells and a melodic bass walk |
| chill_008 | Paper Lanterns | 2 | 76 | D# lydian | zen, peaceful, meditation | Floating lydian lantern-light with shimmering overtones and barely-there percussion |
| chill_009 | Lavender Fields | 2 | 82 | F# major | garden, calm, soft | Pastoral meadow soundscape with singing pads and tiny melodic blossoms |
| chill_010 | Moonlit Canopy | 1 | 70 | C# dorian | zen, meditation, soft | Deep forest night ambience with owl-like calls and a hypnotic dorian drone |
| chill_011 | Drifting Kites | 3 | 100 | G# pentatonic major | breeze, cheerful, easy | Breezy pentatonic float with a playful pluck melody and airy pad washes |
| chill_012 | Still Waters | 1 | 74 | A# major | calm, peaceful, meditation | Mirror-flat lake ambience with slow reverb tails and barely audible melodic whispers |
| chill_013 | Silken Thread | 3 | 96 | D mixolydian | smooth, lounge, easy | Sophisticated mixolydian lounge with buttery chords and a cool walking bass |
| chill_014 | Porcelain Sky | 2 | 84 | E lydian | cloud, zen, peaceful | Delicate lydian sky with bell-like plucks floating over an endless pad horizon |
| chill_015 | Terracotta Noon | 4 | 105 | A dorian | smooth, lounge, relax | Sun-baked dorian groove with a loose beat, mellow keys, and an easy bass line |
| chill_016 | Snowfall Waltz | 2 | 88 | F major | soft, calm, peaceful | Gentle snowfall waltz with twinkling bells and a feather-light three-beat feel |
| chill_017 | Jasmine Tea | 2 | 78 | G pentatonic major | zen, meditation, soft | Meditative pentatonic ritual with ceramic-toned plucks and warm bass hum |
| chill_018 | Indigo Hour | 3 | 98 | B dorian | smooth, lounge, sunset | Deep dorian twilight groove with a muted lead, lush pads, and subtle swing |
| chill_019 | Floating Feather | 1 | 70 | C lydian | cloud, peaceful, meditation | Weightless lydian drift with overtone-rich pads and the faintest melodic outline |
| chill_020 | Harbor Lights | 4 | 110 | F# mixolydian | shore, smooth, relax | Warm harbor-side mixolydian jam with a steady pulse and reflective melodic phrases |
---
### Intense (20 songs)
Energy range: 8-10
Best for: Racing, action, time pressure
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| intense_001 | Midnight Chase | 9 | 160 | C minor | chase, urgent, adrenaline | Heart-pounding minor key pursuit with relentless four-on-floor and staccato stabs |
| intense_002 | Fury Unleashed | 10 | 175 | D phrygian | fury, aggressive, rampage | Phrygian berserker rage with distorted bass, frantic leads, and pummeling kicks |
| intense_003 | Overdrive | 9 | 155 | E minor | overdrive, fast, thrilling | Redlined engine energy with screaming synths and a breakneck tempo that never lets up |
| intense_004 | Razor Edge | 10 | 170 | F harmonic minor | danger, aggressive, combat | Razor-sharp harmonic minor slashes with searing leads and machine-gun percussion |
| intense_005 | Panic Protocol | 9 | 165 | G minor | panic, urgent, pursuit | Emergency klaxon energy with alarm-like leads and a propulsive bass engine |
| intense_006 | Blitz Reaper | 10 | 180 | A phrygian | blitz, fierce, frenzy | Maximum-speed phrygian onslaught with unrelenting double-time drums and savage riffs |
| intense_007 | Neon Inferno | 9 | 150 | B blues | adrenaline, thrilling, fast | Blues-infused neon hellscape with gritty lead bends and a stomping groove |
| intense_008 | Shockwave | 10 | 172 | C# harmonic minor | combat, aggressive, fierce | Explosive harmonic minor shockblast with staccato stabs and obliterating drops |
| intense_009 | Terminal Velocity | 9 | 162 | D# minor | fast, pursuit, adrenaline | Free-falling velocity rush with whipping arpeggios and an unstoppable beat |
| intense_010 | Venom Strike | 10 | 168 | F# phrygian | danger, combat, rampage | Venomous phrygian attack with serpentine lead phrases and toxic bass drops |
| intense_011 | Wraith Runner | 9 | 156 | G# harmonic minor | chase, thrilling, urgent | Ghostly harmonic minor sprint with phasing leads and a thundering rhythmic foundation |
| intense_012 | Meltdown | 10 | 176 | A# chromatic | frenzy, aggressive, panic | Nuclear meltdown chaos with chromatic madness and total percussive devastation |
| intense_013 | Iron Lung | 8 | 148 | C blues | fierce, thrilling, overdrive | Breathless blues-scale assault with a heavy swing and gritty overdriven bass |
| intense_014 | Voltage Surge | 9 | 158 | D minor | adrenaline, fast, urgent | Electric voltage spike with buzzing FM leads and an unstoppable rhythmic charge |
| intense_015 | Reckless Descent | 9 | 152 | E harmonic minor | danger, thrilling, pursuit | Spiraling harmonic minor descent with cascading leads and hammering kicks |
| intense_016 | Firestorm Protocol | 10 | 174 | F phrygian | combat, aggressive, blitz | Scorched-earth phrygian devastation with relentless snare rolls and wall-of-sound bass |
| intense_017 | Cascade Fury | 8 | 145 | G minor | fierce, pursuit, thrilling | Cascading minor fury with tumbling melodic runs and a rock-solid groove foundation |
| intense_018 | Deathline Express | 10 | 178 | A harmonic minor | frenzy, rampage, aggressive | Runaway-train harmonic minor express with breakneck speed and zero stopping power |
| intense_019 | Concrete Jungle | 8 | 142 | B blues | urgent, adrenaline, thrilling | Gritty urban blues assault with dirty bass growls and relentless breakbeat energy |
| intense_020 | Event Horizon | 9 | 164 | D# chromatic | danger, panic, fast | Reality-warping chromatic vortex pulling everything into a relentless rhythmic singularity |
---
### Playful (20 songs)
Energy range: 4-7
Best for: Kids games, casual games, fun moments
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| playful_001 | Bubble Pop | 6 | 130 | C major | fun, bouncy, bubble | Bouncy pop confection with fizzy pluck melodies and a toe-tapping beat |
| playful_002 | Candy Rush | 7 | 140 | D mixolydian | candy, happy, party | Sugar-coated mixolydian romp with rapid-fire melodies and a party-floor beat |
| playful_003 | Skip & Jump | 5 | 120 | F major | skip, cheerful, lighthearted | Carefree skip-along with a lilting melody, simple chords, and a playful shuffle |
| playful_004 | Sunshine Express | 6 | 135 | G lydian | sunshine, happy, dance | Bright lydian sunshine ride with sparkling chords and an infectious rhythmic hook |
| playful_005 | Frog Hop | 5 | 118 | A pentatonic major | jump, silly, cartoon | Quirky pentatonic hop with bouncing bass notes and a whimsical plucked melody |
| playful_006 | Pixel Parade | 6 | 132 | E major | fun, cheerful, party | Retro pixel-art parade with chiptuney leads and a marching festival vibe |
| playful_007 | Jellybean Bounce | 7 | 142 | B mixolydian | bouncy, candy, dance | Maximum-bounce mixolydian groove with rubbery bass and a sugary melodic hook |
| playful_008 | Rainbow Sprinkles | 5 | 125 | C# lydian | rainbow, happy, lighthearted | Colorful lydian celebration with chiming leads and a gentle, flowing groove |
| playful_009 | Trampoline | 7 | 145 | D# major | jump, bouncy, fun | Sky-high trampoline energy with elastic bass and soaring staccato rebounds |
| playful_010 | Whirlwind Waltz | 4 | 115 | F# major | dance, cheerful, lighthearted | Gentle whirlwind waltz with graceful plucks and a charming three-beat lilt |
| playful_011 | Firefly Festival | 5 | 122 | G# pentatonic major | cheerful, fun, frolic | Twinkling firefly night with dancing pentatonic melodies and a warm, steady pulse |
| playful_012 | Lollipop Lane | 6 | 128 | A# major | candy, silly, cartoon | Sweet lollipop stroll with cartoon-like sound effects and a skip-worthy rhythm |
| playful_013 | Dizzy Spin | 7 | 148 | E dorian | fun, dance, party | Dizzying dorian spin cycle with whirling arpeggios and a head-bobbing groove |
| playful_014 | Cotton Candy Clouds | 4 | 112 | F lydian | cloud, happy, lighthearted | Fluffy lydian cotton-candy float with soft plucks and dreamy pad wisps |
| playful_015 | Pinball Wizard | 7 | 150 | A mixolydian | fun, bouncy, party | Rapid-fire pinball ricochet with dinging FM leads and a hyperactive bassline |
| playful_016 | Confetti Cannon | 6 | 138 | B major | party, cheerful, dance | Explosive confetti burst with bright staccato hits and a celebratory chorus |
| playful_017 | Rubber Duck | 5 | 116 | D pentatonic major | silly, cartoon, fun | Squeaky-clean pentatonic ditty with rubbery bass plucks and a goofy swing |
| playful_018 | Kite Runner | 5 | 124 | G dorian | frolic, lighthearted, breeze | Breezy dorian kite flight with soaring lead phrases and a playful rhythmic tug |
| playful_019 | Pancake Flip | 6 | 134 | C mixolydian | silly, fun, bouncy | Morning-kitchen mixolydian jam with flippy rhythms and a warm, buttery tone |
| playful_020 | Starlight Carousel | 4 | 110 | F# lydian | dance, happy, lighthearted | Enchanted carousel spin with twinkling lydian bells and a gentle, revolving rhythm |
---
### Mysterious (20 songs)
Energy range: 2-5
Best for: Horror, exploration, puzzles
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| mysterious_001 | Shadow Waltz | 3 | 95 | D phrygian | mystery, dark, suspense | Eerie waltz with unsettling chromatic turns and ghostly reverberations |
| mysterious_002 | Phantom's Riddle | 4 | 105 | A harmonic minor | phantom, riddle, enigma | Brooding harmonic minor theme with restless melodic questions |
| mysterious_003 | Twilight Labyrinth | 3 | 88 | F# minor | twilight, labyrinth, shadow | Winding minor key passages through dim corridors of sound |
| mysterious_004 | Crypt Whispers | 2 | 82 | B phrygian | crypt, whisper, eerie | Hushed phrygian murmurs echoing through ancient stone chambers |
| mysterious_005 | Fog of Enigma | 3 | 100 | E whole tone | fog, enigma, mystery | Drifting whole-tone haze obscuring all sense of harmonic direction |
| mysterious_006 | Spell Unraveled | 5 | 115 | G harmonic minor | spell, dark, haunt | Urgent harmonic minor incantation building to a feverish climax |
| mysterious_007 | Void Gazer | 2 | 84 | C chromatic | void, dark, eerie | Staring into the abyss with atonal fragments dissolving into silence |
| mysterious_008 | Detective Noir | 4 | 108 | C# minor | detective, suspense, shadow | Smoky noir investigation theme with sly chromatic bass lines |
| mysterious_009 | Haunted Carousel | 4 | 112 | F phrygian | haunt, eerie, puzzle | A broken carousel spinning in phrygian darkness with warped melodies |
| mysterious_010 | Riddle of Echoes | 3 | 92 | D# harmonic minor | riddle, enigma, whisper | Haunting call-and-response between shadowy melodic fragments |
| mysterious_011 | Obsidian Mirror | 3 | 98 | G# minor | dark, mystery, shadow | Dark reflections shimmer in minor key with glassy sustained tones |
| mysterious_012 | Serpent Passage | 5 | 118 | A# phrygian | suspense, dark, labyrinth | Slithering phrygian riff coiling through an underground labyrinth |
| mysterious_013 | Moonless Night | 2 | 80 | E whole tone | eerie, void, twilight | Formless whole-tone drifts under a starless sky with no resolution |
| mysterious_014 | Specter's Descent | 4 | 104 | B harmonic minor | phantom, dark, haunt | Descending harmonic minor spiral pulling deeper into spectral depths |
| mysterious_015 | Puzzle Chamber | 4 | 110 | F minor | puzzle, mystery, riddle | Interlocking melodic puzzles clicking into place within stone walls |
| mysterious_016 | Veiled Threshold | 3 | 90 | C phrygian | mystery, fog, twilight | Standing at the boundary between worlds in thick phrygian fog |
| mysterious_017 | Waning Sigil | 3 | 96 | D harmonic minor | spell, enigma, crypt | A fading magical sigil pulsing with diminishing harmonic minor energy |
| mysterious_018 | Forgotten Archive | 3 | 94 | G# chromatic | mystery, puzzle, shadow | Dusty chromatic fragments unearthed from a lost archive of secrets |
| mysterious_019 | Oracle's Warning | 5 | 120 | A minor | suspense, dark, whisper | An urgent prophetic warning delivered in frantic minor key passages |
| mysterious_020 | Abyssal Lullaby | 2 | 85 | F# whole tone | void, eerie, twilight | A gentle yet deeply unsettling lullaby drifting over an endless abyss |
---
### Heroic (20 songs)
Energy range: 6-9
Best for: Adventure, achievements, victories
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| heroic_001 | Champion's Anthem | 8 | 145 | C major | hero, triumph, anthem | A soaring major key anthem celebrating the ultimate champion |
| heroic_002 | Dawn of Valor | 7 | 138 | G lydian | dawn, valor, courage | Radiant lydian sunrise heralding a new era of bravery and valor |
| heroic_003 | Shield Bearer | 7 | 132 | D mixolydian | shield, brave, quest | Sturdy mixolydian march of a steadfast shield bearer on a noble quest |
| heroic_004 | Glory Unchained | 9 | 155 | A major | glory, triumph, champion | Explosive high-energy celebration of total victory and unchained glory |
| heroic_005 | Oath of the Paladin | 7 | 128 | F dorian | oath, paladin, courage | Solemn yet powerful dorian oath sworn by a noble paladin |
| heroic_006 | Crusader's March | 8 | 142 | E major | crusade, hero, sword | Relentless marching rhythm driving a crusade forward to victory |
| heroic_007 | Beacon of Hope | 6 | 125 | B lydian | beacon, hero, legend | A luminous lydian melody shining as a beacon through the darkness |
| heroic_008 | Thunder Vanguard | 9 | 158 | D# minor | brave, quest, sword | Thunderous minor key charge of the vanguard into the heart of battle |
| heroic_009 | Skyward Bound | 7 | 135 | F# major | adventure, glory, dawn | An uplifting ascent into the clouds with boundless major key optimism |
| heroic_010 | Iron Resolve | 8 | 148 | C# dorian | champion, shield, valor | Unbreakable dorian determination with a hammering rhythmic backbone |
| heroic_011 | Wingspan | 6 | 122 | G# lydian | adventure, hero, dawn | Soaring lydian theme evoking the spread of mighty wings at first light |
| heroic_012 | Legend's Forge | 8 | 150 | A# minor | legend, sword, crusade | White-hot minor key intensity of a legendary weapon being forged |
| heroic_013 | Rally Cry | 9 | 160 | D mixolydian | triumph, anthem, glory | A deafening mixolydian rally cry uniting an army before the final charge |
| heroic_014 | Valor Rising | 7 | 130 | F major | valor, brave, quest | A steadily rising major key theme embodying growing courage |
| heroic_015 | Fortress Eternal | 7 | 136 | B dorian | shield, paladin, oath | Impenetrable dorian fortress theme with layered harmonic defenses |
| heroic_016 | Phoenix's Flight | 8 | 146 | E lydian | hero, legend, dawn | Blazing lydian ascent of a reborn phoenix soaring above the flames |
| heroic_017 | Banner Unfurled | 7 | 134 | G major | glory, courage, champion | Majestic unfurling of a victorious banner in crisp major key splendor |
| heroic_018 | Dragonslayer | 9 | 156 | C# minor | sword, brave, quest | Ferocious minor key battle theme of a legendary dragonslayer |
| heroic_019 | Crowning Moment | 6 | 120 | A# major | triumph, anthem, glory | A stately coronation theme building to an emotional crowning climax |
| heroic_020 | Eternal Vow | 6 | 126 | F# mixolydian | oath, paladin, beacon | A profound mixolydian pledge of eternal service and unwavering loyalty |
---
### Quirky (20 songs)
Energy range: 5-8
Best for: Comedy, unique games, surprises
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| quirky_001 | Wobble Walk | 6 | 118 | C mixolydian | wobble, funny, offbeat | A tipsy mixolydian stroll with syncopated bass and bouncy staccato hits |
| quirky_002 | Glitch Parade | 7 | 128 | F blues | glitch, weird, surprise | A malfunctioning parade of blues riffs and unexpected rhythmic hiccups |
| quirky_003 | Mischief Maker | 6 | 112 | D dorian | mischief, prank, eccentric | Sneaky dorian theme of a trickster plotting delightful chaos |
| quirky_004 | Rubber Duck Boogie | 7 | 132 | A pentatonic minor | funny, bounce, comedy | Absurd pentatonic boogie with squeaky lead lines and rubber bass |
| quirky_005 | Clockwork Confusion | 5 | 108 | G whole tone | odd, twist, eccentric | Malfunctioning clockwork mechanism ticking in whole-tone disorientation |
| quirky_006 | Prankster Polka | 7 | 135 | E mixolydian | prank, wacky, clown | A manic polka-inspired romp driven by wacky syncopation and flat-seven swagger |
| quirky_007 | Jellybean Jamboree | 8 | 140 | B lydian | bounce, zany, comedy | Sugary lydian explosion of color and bouncing melodic candy |
| quirky_008 | Sneaky Squirrel | 5 | 105 | D# dorian | mischief, trick, offbeat | Furtive dorian scurrying with darting staccato leads and syncopated bass |
| quirky_009 | Topsy Turvy | 6 | 120 | F# blues | chaos, twist, wacky | Everything flipped upside down in a chaotic blues-scale adventure |
| quirky_010 | Noodle Soup | 5 | 102 | C# pentatonic minor | weird, odd, eccentric | A winding noodly pentatonic melody sloshing around in rhythmic broth |
| quirky_011 | Pixel Pandemonium | 8 | 138 | G# mixolydian | glitch, chaos, surprise | Hyper 8-bit style mixolydian mayhem with frantic arpeggio bursts |
| quirky_012 | Banana Peel Slide | 6 | 116 | A# blues | funny, clown, comedy | Slippery blues-scale comedy with pratfall rhythms and slide-whistle leads |
| quirky_013 | Funhouse Mirror | 5 | 110 | E whole tone | twist, odd, eccentric | Distorted whole-tone reflections warping reality in a carnival funhouse |
| quirky_014 | Hiccup Hop | 7 | 126 | D dorian | bounce, wacky, zany | Jerky dorian hop interrupted by melodic hiccups and rhythmic stumbles |
| quirky_015 | Zigzag Zephyr | 6 | 122 | B lydian | surprise, offbeat, mischief | Unpredictable lydian wind gusts pushing melodies in zigzag directions |
| quirky_016 | Rubber Band Riff | 6 | 114 | F pentatonic minor | wobble, trick, comedy | Stretchy pentatonic riffs snapping back and forth with elastic energy |
| quirky_017 | Carousel Malfunction | 7 | 130 | G mixolydian | chaos, glitch, wacky | A malfunctioning fairground ride spinning at odd intervals in mixolydian frenzy |
| quirky_018 | Jester Jig | 7 | 136 | A dorian | clown, prank, zany | A flamboyant dorian jig performed by a mischievous court jester |
| quirky_019 | Slapstick Serenade | 5 | 100 | C# blues | funny, comedy, twist | A tender serenade gone hilariously wrong with slapstick timing and blue notes |
| quirky_020 | Kaleidoscope Caper | 6 | 124 | D# lydian | surprise, eccentric, wobble | Shifting lydian patterns like a kaleidoscope of musical fragments |
---
### Ambient (20 songs)
Energy range: 1-3
Best for: Background, meditation, creative tools
| ID | Name | Energy | Tempo | Key | Tags | Description |
|----|------|--------|-------|-----|------|-------------|
| ambient_001 | Nebula Drift | 2 | 68 | C major | space, cosmic, floating | Weightless major key pads drifting through a colorful cosmic nebula |
| ambient_002 | Crystal Meadow | 2 | 72 | G pentatonic major | crystal, meadow, tranquil | Sparkling pentatonic chimes scattered across a sunlit meadow |
| ambient_003 | Glacial Horizon | 1 | 62 | D lydian | glacier, vast, ethereal | Immense glacial soundscape stretching to an infinite lydian horizon |
| ambient_004 | Aurora Veil | 2 | 75 | A pentatonic major | aurora, ethereal, dream | Shimmering aurora borealis painted in pentatonic washes of light |
| ambient_005 | Tidal Murmur | 2 | 66 | F minor | tide, mist, tranquil | Gentle minor key tides lapping rhythmically in a misty coastal haze |
| ambient_006 | Stardust Canopy | 1 | 60 | B major | stardust, cosmic, vast | An endless canopy of stars rendered in slow-moving major key pads |
| ambient_007 | Silk River | 2 | 70 | E pentatonic minor | floating, dream, tranquil | Smooth pentatonic minor currents flowing like liquid silk through space |
| ambient_008 | Morning Dew | 2 | 76 | D# lydian | meadow, crystal, atmosphere | Dewdrops catching first light in shimmering lydian bell tones |
| ambient_009 | Deep Cavern Echo | 1 | 64 | F# minor | void, vast, atmosphere | Cavernous minor key echoes bouncing endlessly through underground spaces |
| ambient_010 | Velvet Cosmos | 2 | 74 | C# whole tone | cosmic, nebula, floating | Luxurious whole-tone textures unfolding across a velvet cosmic canvas |
| ambient_011 | Feather Fall | 1 | 63 | G# pentatonic major | ethereal, dream, floating | Weightless pentatonic notes falling like feathers through still air |
| ambient_012 | Horizon Glow | 3 | 82 | A# major | horizon, aurora, tranquil | Warm major key glow spreading across a peaceful distant horizon |
| ambient_013 | Moss Garden | 2 | 69 | D pentatonic minor | meadow, mist, tranquil | A damp moss garden where pentatonic tones grow like delicate plants |
| ambient_014 | Solar Wind | 2 | 78 | F lydian | space, cosmic, vast | Streams of lydian particles carried by an invisible solar wind |
| ambient_015 | Moonpool Reflection | 1 | 61 | B minor | dream, mist, ethereal | Perfectly still moonlit pool reflecting minor key pads in mirrored silence |
| ambient_016 | Ember Glow | 3 | 85 | E pentatonic major | atmosphere, tranquil, meadow | Warm dying embers casting a pentatonic glow across a quiet room |
| ambient_017 | Cloud Temple | 2 | 71 | A lydian | floating, ethereal, vast | An ancient temple floating among the clouds in shimmering lydian suspension |
| ambient_018 | Frozen Bloom | 1 | 65 | G whole tone | glacier, crystal, void | Ice crystals blooming in slow motion with directionless whole-tone shimmer |
| ambient_019 | Whispering Sands | 2 | 73 | C# pentatonic minor | tide, mist, atmosphere | Desert sands whispering pentatonic secrets carried on a warm breeze |
| ambient_020 | Infinite Stillness | 1 | 60 | F# major | void, stardust, vast | Absolute stillness at the edge of the universe where sound barely exists |
---
## Compatibility Matrix
### Mood to Background Theme
| Mood | Excellent Themes | Good Themes | Avoid |
|------|-----------------|-------------|-------|
| Epic | space, fantasy, dungeon, volcanic | urban, stadium, castle | pastel, garden, beach |
| Chill | garden, beach, sunset, meadow | forest, sky, underwater | dungeon, volcanic, industrial |
| Intense | urban, industrial, volcanic, stadium | space, dungeon, desert | garden, beach, meadow |
| Playful | candy, circus, playground, cartoon | beach, forest, sky | dungeon, volcanic, horror |
| Mysterious | dungeon, forest-night, underwater-deep, ancient-ruins | castle, space-dark, swamp | candy, playground, beach |
| Heroic | castle, stadium, mountain-peak, sky | fantasy, forest, space | dungeon, horror, swamp |
| Quirky | circus, cartoon, laboratory, toy-room | urban, candy, beach | dungeon, horror, volcanic |
| Ambient | space, underwater, forest, sky | desert, beach, meadow | urban, stadium, industrial |
### Mood to Avatar Context
| Mood | Recommended Contexts |
|------|---------------------|
| Epic | knight-armor, spaceship-cockpit, dragon-rider, warrior-arena, commander-bridge |
| Chill | beach-chair, hammock, cafe-window, meditation-cushion, garden-bench |
| Intense | race-car, fighter-jet, motorcycle, battle-station, control-room |
| Playful | playground, trampoline, carnival-booth, ball-pit, treehouse |
| Mysterious | detective-office, ancient-library, foggy-alley, crystal-cave, haunted-mansion |
| Heroic | throne-room, mountain-summit, victory-podium, ship-helm, castle-balcony |
| Quirky | mad-scientist-lab, upside-down-room, funhouse, time-machine, cartoon-world |
| Ambient | starship-window, underwater-dome, cloud-platform, zen-garden, space-station |
---
## API Usage
```javascript
// Initialize (called automatically on first play)
await MusicEngine.init();
// Play random song from mood
const song = await MusicEngine.playRandomSong('Epic');
console.log(song.name); // e.g., "Siege of Thunder"
// Play specific song by ID
const song = await MusicEngine.playSong('epic_001');
// Find songs by criteria
const songs = MusicEngine.findSongs({
mood: 'Epic',
minEnergy: 8,
maxEnergy: 10,
minTempo: 150,
maxTempo: 180,
tags: ['battle']
});
// Get full catalog with metadata
const allSongs = MusicEngine.getCatalog();
// Returns: [{ id, name, mood, energy, tempo, key, scale, tags, desc }, ...]
// Volume control (0.0 to 1.0)
MusicEngine.setVolume(0.7);
// Fade out current song (duration in seconds)
MusicEngine.fadeOut(2);
// Stop music immediately
MusicEngine.stopMusic();
// Check current state
const isPlaying = MusicEngine.isPlaying();
const currentSong = MusicEngine.getCurrentSong();
// Get all available moods
const moods = MusicLibrary.getMoods();
// Returns: ['Epic', 'Chill', 'Intense', 'Playful', 'Mysterious', 'Heroic', 'Quirky', 'Ambient']
// Get songs for a specific mood
const epicSongs = MusicLibrary.getSongsByMood('Epic');
// Get a specific song definition
const song = MusicLibrary.getSong('epic_001');
```
---
## Technical Details
### Synthesis
- **Engine**: Tone.js (loaded from CDN)
- **Duration**: 60+ seconds per song (loops seamlessly)
- **Sample Rate**: 44.1 kHz (browser default)
### Synth Types Used
| Type | Tone.js Class | Typical Use |
|------|---------------|-------------|
| FM | FMSynth | Bright leads, stabs, bells |
| AM | AMSynth | Warm pads, subtle leads |
| Mono | MonoSynth | Bass, monophonic leads |
| Poly | PolySynth | Pads, chords |
| Membrane | MembraneSynth | Drums, percussive bass |
| Metal | MetalSynth | Metallic percussion |
| Pluck | PluckSynth | Plucked strings, mallets |
| Duo | DuoSynth | Rich leads, doubled tones |
| Noise | NoiseSynth | Effects, ambience |
### Effects Chain
| Effect | Parameter | Range | Purpose |
|--------|-----------|-------|---------|
| Reverb | reverb | 0.1-0.8 | Room size/tail |
| Delay | delay | 0.05-0.5 | Echo amount |
| Filter | filter | 500-8000 | Low-pass cutoff (Hz) |
### Song Structure
Each song defines:
- **Sections**: intro, verse, chorus, bridge, break, outro
- **Per Section**: bars, chord progression, melody style, bass style, drum pattern, intensity
### Scales Available
| Scale | Intervals | Typical Mood |
|-------|-----------|--------------|
| major | 0,2,4,5,7,9,11 | Happy, triumphant |
| minor | 0,2,3,5,7,8,10 | Sad, dramatic |
| dorian | 0,2,3,5,7,9,10 | Jazzy, mysterious |
| phrygian | 0,1,3,5,7,8,10 | Dark, exotic |
| lydian | 0,2,4,6,7,9,11 | Dreamy, bright |
| mixolydian | 0,2,4,5,7,9,10 | Bluesy, groovy |
| harmonicMinor | 0,2,3,5,7,8,11 | Dramatic, Eastern |
| melodicMinor | 0,2,3,5,7,9,11 | Jazz, sophisticated |
| pentatonicMajor | 0,2,4,7,9 | Simple, folk |
| pentatonicMinor | 0,3,5,7,10 | Blues, rock |
| blues | 0,3,5,6,7,10 | Blues, funky |
| wholeTone | 0,2,4,6,8,10 | Dreamy, unsettling |
| chromatic | all 12 | Atonal, chaotic |
---
## Statistics Summary
| Mood | Songs | Energy Range | Tempo Range | Top Tags |
|------|-------|--------------|-------------|----------|
| Epic | 20 | 7-10 | 120-170 | battle, heroic, powerful |
| Chill | 20 | 1-4 | 70-110 | peaceful, calm, soft |
| Intense | 20 | 8-10 | 142-180 | aggressive, fast, urgent |
| Playful | 20 | 4-7 | 110-150 | fun, bouncy, happy |
| Mysterious | 20 | 2-5 | 80-120 | dark, enigma, eerie |
| Heroic | 20 | 6-9 | 120-160 | triumph, courage, glory |
| Quirky | 20 | 5-8 | 100-140 | funny, wacky, eccentric |
| Ambient | 20 | 1-3 | 60-85 | floating, tranquil, vast |
**Total: 160 songs**
+546
View File
@@ -0,0 +1,546 @@
# Preset Guide
## Overview
220 curated asset combinations providing instant variety for 11 game styles.
Each preset includes coordinated music, background, and avatar context.
**Total Presets:** 220
**Presets per Style:** 20
**Game Styles:** 11
---
## Game Styles
### Action/Arcade (20 presets)
Fast-paced action games with intense gameplay and dynamic visuals.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| action-space-01 | Space Hero | Epic, 9 | space/nebula | spaceship-cockpit | Space shooters |
| action-space-02 | Asteroid Runner | Intense, 8 | space/asteroidField | spaceship-cockpit | Dodging games |
| action-platform-01 | Retro Runner | Playful, 7 | retro/pixelGrid | platformer-standard | Platformers |
| action-platform-02 | Jungle Dash | Heroic, 8 | nature/jungle | adventure-hero | Jungle runners |
| action-tank-01 | Tank Commander | Heroic, 9 | urban/citySkyline | tank | Military games |
| action-tank-02 | Desert Storm | Epic, 8 | nature/desert | tank | Desert warfare |
| action-mech-01 | Mech Warrior | Epic, 10 | mechanical/robotFactory | mech-suit | Robot battles |
| action-mech-02 | Titan Fall | Intense, 9 | space/planets | mech-suit | Sci-fi combat |
| action-jetpack-01 | Jetpack Joyride | Playful, 7 | sky/clouds | jetpack | Flying games |
| action-jetpack-02 | Volcano Escape | Intense, 9 | nature/mountains | jetpack | Escape games |
| action-ninja-01 | Shadow Strike | Mysterious, 7 | fantasy/temple | ninja-outfit | Stealth action |
| action-ninja-02 | Rooftop Runner | Intense, 8 | urban/rooftop | ninja-outfit | Parkour games |
| action-shooter-01 | Alien Blaster | Epic, 9 | space/deepSpace | spaceship-cockpit | Shoot-em-ups |
| action-shooter-02 | Robot Rampage | Intense, 8 | mechanical/factory | superhero-cape | Wave shooters |
| action-flying-01 | Sky Ace | Heroic, 8 | sky/flyingHigh | airplane | Dogfight games |
| action-flying-02 | Storm Chaser | Intense, 9 | weather/storm | airplane | Weather dodging |
| action-underwater-01 | Deep Sea Diver | Mysterious, 6 | underwater/deepSea | submarine | Ocean exploration |
| action-underwater-02 | Submarine Command | Heroic, 7 | underwater/submarine | submarine | Naval combat |
| action-superhero-01 | Cape Crusader | Heroic, 9 | urban/citySkyline | superhero-cape | Superhero games |
| action-arcade-classic | Arcade Classic | Playful, 8 | retro/arcadeCabinet | platformer-standard | Retro arcade |
---
### Puzzle (20 presets)
Brain-teasing puzzle games with calm, focused atmospheres.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| puzzle-zen-01 | Zen Garden | Chill, 2 | nature/meadow | platformer-standard | Matching games |
| puzzle-zen-02 | Bamboo Sanctuary | Ambient, 2 | nature/forest | platformer-standard | Tile puzzles |
| puzzle-abstract-01 | Geometric Dreams | Ambient, 3 | abstract/geometricPatterns | platformer-standard | Pattern games |
| puzzle-abstract-02 | Color Flow | Chill, 3 | abstract/gradients | platformer-standard | Color matching |
| puzzle-lab-01 | Science Lab | Quirky, 4 | indoor/laboratory | scientist-labcoat | Chemistry puzzles |
| puzzle-lab-02 | Brain Teasers | Quirky, 4 | indoor/library | scientist-labcoat | Logic puzzles |
| puzzle-colorful-01 | Candy Crush | Playful, 5 | colorful/candyWorld | platformer-standard | Match-3 games |
| puzzle-colorful-02 | Rainbow Bridge | Playful, 4 | colorful/rainbow | platformer-standard | Color puzzles |
| puzzle-space-01 | Cosmic Puzzle | Ambient, 3 | space/nebula | space-suit | Space puzzles |
| puzzle-space-02 | Starlight Logic | Mysterious, 3 | space/deepSpace | space-suit | Constellation puzzles |
| puzzle-library-01 | Ancient Library | Mysterious, 4 | fantasy/magicalLibrary | wizard-robes | Word puzzles |
| puzzle-library-02 | Scholar's Study | Chill, 3 | indoor/library | wizard-robes | Book puzzles |
| puzzle-ocean-01 | Ocean Depths | Ambient, 2 | underwater/coralReef | platformer-standard | Marine puzzles |
| puzzle-ocean-02 | Bubble Pop | Playful, 4 | underwater/kelpForest | platformer-standard | Bubble games |
| puzzle-crystal-01 | Crystal Cave | Mysterious, 4 | fantasy/crystalCave | platformer-standard | Gem puzzles |
| puzzle-crystal-02 | Jewel Quest | Playful, 5 | fantasy/crystalCave | adventure-hero | Gem matching |
| puzzle-cloud-01 | Cloud Kingdom | Ambient, 2 | sky/clouds | platformer-standard | Sky puzzles |
| puzzle-cloud-02 | Dream Weaver | Chill, 2 | sky/horizon | platformer-standard | Dream puzzles |
| puzzle-minimalist-01 | Minimal Mind | Ambient, 2 | abstract/minimalist | platformer-standard | Minimal puzzles |
| puzzle-garden-01 | Garden Puzzle | Chill, 3 | seasonal/springMeadow | platformer-standard | Nature puzzles |
---
### Story Adventure (20 presets)
Narrative-driven adventures with immersive world-building.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| story-fantasy-01 | Dragon Quest | Heroic, 8 | fantasy/castle | knight-armor | Fantasy RPGs |
| story-fantasy-02 | Enchanted Forest | Mysterious, 6 | fantasy/enchantedForest | adventure-hero | Fairy tales |
| story-wizard-01 | Wizard Academy | Quirky, 6 | fantasy/magicalLibrary | wizard-robes | Magic schools |
| story-wizard-02 | Spell Caster | Epic, 7 | fantasy/tower | wizard-robes | Magic adventures |
| story-pirate-01 | Pirate's Treasure | Heroic, 7 | nature/ocean | pirate-outfit | Pirate adventures |
| story-pirate-02 | Shipwreck Island | Mysterious, 6 | underwater/shipwreck | pirate-outfit | Island mysteries |
| story-nature-01 | Forest Journey | Chill, 5 | nature/forest | adventure-hero | Nature quests |
| story-nature-02 | Mountain Expedition | Heroic, 6 | nature/mountains | adventure-hero | Climbing adventures |
| story-urban-01 | City Detective | Mysterious, 5 | urban/street | platformer-standard | Mystery solving |
| story-urban-02 | Night City | Mysterious, 6 | urban/neonDistrict | ninja-outfit | Noir adventures |
| story-spooky-01 | Haunted Manor | Mysterious, 5 | spooky/hauntedHouse | platformer-standard | Ghost stories |
| story-spooky-02 | Graveyard Shift | Mysterious, 6 | spooky/graveyard | ninja-outfit | Horror adventures |
| story-medieval-01 | Knight's Honor | Heroic, 7 | fantasy/castle | knight-armor | Medieval quests |
| story-medieval-02 | Village Hero | Playful, 6 | fantasy/castle | adventure-hero | Village adventures |
| story-scifi-01 | Space Explorer | Ambient, 5 | space/spaceStation | space-suit | Sci-fi exploration |
| story-scifi-02 | Alien Contact | Mysterious, 6 | space/planets | space-suit | First contact stories |
| story-fairy-01 | Fairy Tale | Playful, 4 | fantasy/enchantedForest | wizard-robes | Classic fairy tales |
| story-fairy-02 | Magic Kingdom | Heroic, 5 | fantasy/floatingIslands | adventure-hero | Fantasy kingdoms |
| story-desert-01 | Desert Wanderer | Ambient, 5 | nature/desert | cowboy-gear | Desert adventures |
| story-desert-02 | Oasis Quest | Heroic, 6 | nature/desert | adventure-hero | Desert exploration |
---
### Racing (20 presets)
High-speed racing games with adrenaline-pumping action.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| racing-street-01 | Street Racer | Intense, 9 | urban/street | race-car | Street racing |
| racing-street-02 | Midnight Run | Intense, 8 | urban/neonDistrict | race-car | Night racing |
| racing-track-01 | Grand Prix | Epic, 9 | sports/raceTrack | race-car | Track racing |
| racing-track-02 | Speed Champion | Heroic, 9 | sports/stadium | race-car | Championship races |
| racing-motorcycle-01 | Moto Mayhem | Intense, 9 | urban/street | motorcycle | Motorcycle racing |
| racing-motorcycle-02 | Highway Rider | Heroic, 8 | nature/mountains | motorcycle | Highway racing |
| racing-hover-01 | Hover Rush | Epic, 9 | abstract/waves | hoverboard | Futuristic racing |
| racing-hover-02 | Neon Glide | Intense, 8 | retro/synthwave | hoverboard | Cyberpunk racing |
| racing-runner-01 | Endless Runner | Playful, 7 | urban/street | runner | Endless runners |
| racing-runner-02 | Temple Sprint | Heroic, 8 | fantasy/temple | runner | Temple runs |
| racing-offroad-01 | Offroad Rally | Intense, 8 | nature/mountains | race-car | Rally racing |
| racing-offroad-02 | Mud Madness | Heroic, 7 | nature/forest | race-car | Offroad racing |
| racing-boat-01 | Wave Rider | Heroic, 8 | nature/ocean | platformer-standard | Boat racing |
| racing-boat-02 | River Rush | Intense, 7 | nature/waterfall | platformer-standard | River racing |
| racing-sky-01 | Sky Race | Epic, 9 | sky/flyingHigh | airplane | Air racing |
| racing-sky-02 | Cloud Dash | Playful, 7 | sky/clouds | flappy-style | Sky dodging |
| racing-kart-01 | Kart Kingdom | Playful, 7 | colorful/rainbow | race-car | Kart racing |
| racing-kart-02 | Turbo Toons | Playful, 8 | colorful/candyWorld | race-car | Cartoon racing |
| racing-winter-01 | Snow Drift | Heroic, 7 | seasonal/winterWonderland | race-car | Winter racing |
| racing-winter-02 | Ice Racer | Intense, 8 | weather/snow | race-car | Ice track racing |
---
### Pet/Simulator (20 presets)
Relaxing pet care and life simulation games.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| pet-home-01 | Cozy Home | Chill, 3 | indoor/bedroom | platformer-standard | Virtual pets |
| pet-home-02 | Pet Palace | Playful, 4 | indoor/bedroom | platformer-standard | Pet mansions |
| pet-garden-01 | Garden Friends | Chill, 3 | nature/meadow | platformer-standard | Outdoor pets |
| pet-garden-02 | Butterfly Garden | Ambient, 2 | seasonal/springMeadow | platformer-standard | Insect pets |
| pet-farm-01 | Farm Life | Playful, 4 | nature/meadow | platformer-standard | Farm animals |
| pet-farm-02 | Barn Buddies | Playful, 5 | nature/forest | platformer-standard | Farm simulation |
| pet-aquarium-01 | Aqua World | Ambient, 2 | underwater/coralReef | platformer-standard | Fish tanks |
| pet-aquarium-02 | Deep Sea Friends | Chill, 3 | underwater/kelpForest | platformer-standard | Ocean pets |
| pet-park-01 | Pet Park | Playful, 4 | urban/park | platformer-standard | Dog walking |
| pet-park-02 | Playground Pals | Playful, 5 | urban/park | platformer-standard | Pet playgrounds |
| pet-cafe-01 | Pet Cafe | Chill, 3 | indoor/kitchen | chef-outfit | Cat cafes |
| pet-cafe-02 | Treat Shop | Playful, 4 | colorful/candyWorld | chef-outfit | Pet treats |
| pet-exotic-01 | Exotic Sanctuary | Mysterious, 4 | nature/jungle | adventure-hero | Exotic pets |
| pet-exotic-02 | Rainforest Refuge | Ambient, 3 | nature/jungle | adventure-hero | Jungle animals |
| pet-wildlife-01 | Wildlife Rescue | Heroic, 5 | nature/forest | adventure-hero | Animal rescue |
| pet-wildlife-02 | Safari Friends | Playful, 4 | nature/desert | adventure-hero | Safari animals |
| pet-virtual-01 | Cyber Pet | Quirky, 4 | retro/pixelGrid | platformer-standard | Digital pets |
| pet-virtual-02 | Pixel Pals | Playful, 5 | retro/eightBit | platformer-standard | Retro virtual pets |
| pet-clinic-01 | Vet Clinic | Chill, 4 | indoor/laboratory | scientist-labcoat | Vet games |
| pet-grooming-01 | Grooming Salon | Playful, 3 | colorful/paintSplatter | platformer-standard | Pet grooming |
---
### Trivia/Quiz (20 presets)
Educational quiz games with engaging visual themes.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| trivia-gameshow-01 | Game Show Star | Playful, 6 | colorful/disco | platformer-standard | TV quiz shows |
| trivia-gameshow-02 | Quiz Champion | Epic, 7 | colorful/neon | platformer-standard | Championship quizzes |
| trivia-classroom-01 | Classroom Quiz | Quirky, 4 | indoor/classroom | scientist-labcoat | School quizzes |
| trivia-classroom-02 | Study Session | Chill, 3 | indoor/library | wizard-robes | Study games |
| trivia-space-01 | Space Academy | Mysterious, 5 | space/nebula | space-suit | Astronomy quizzes |
| trivia-space-02 | Cosmic Quiz | Ambient, 4 | space/deepSpace | space-suit | Space trivia |
| trivia-history-01 | Time Traveler | Heroic, 5 | fantasy/castle | knight-armor | History quizzes |
| trivia-history-02 | Ancient Wisdom | Mysterious, 4 | fantasy/temple | wizard-robes | Ancient history |
| trivia-nature-01 | Nature Explorer | Chill, 4 | nature/forest | adventure-hero | Nature quizzes |
| trivia-nature-02 | Wildlife Quiz | Playful, 4 | nature/jungle | adventure-hero | Animal trivia |
| trivia-pop-01 | Pop Culture | Playful, 6 | colorful/neon | platformer-standard | Pop culture quizzes |
| trivia-pop-02 | Music Mania | Playful, 7 | colorful/disco | platformer-standard | Music trivia |
| trivia-sports-01 | Sports Fanatic | Heroic, 6 | sports/stadium | sports-player | Sports quizzes |
| trivia-sports-02 | Champion Trivia | Epic, 7 | sports/arena | sports-player | Sports history |
| trivia-geography-01 | World Explorer | Heroic, 5 | sky/flyingHigh | adventure-hero | Geography quizzes |
| trivia-geography-02 | Map Master | Chill, 4 | sky/horizon | adventure-hero | Map quizzes |
| trivia-food-01 | Foodie Quiz | Playful, 5 | indoor/kitchen | chef-outfit | Food trivia |
| trivia-food-02 | Chef Challenge | Quirky, 6 | colorful/candyWorld | chef-outfit | Cooking quizzes |
| trivia-math-01 | Math Genius | Quirky, 5 | abstract/geometricPatterns | scientist-labcoat | Math quizzes |
| trivia-general-01 | Know It All | Playful, 5 | colorful/rainbow | platformer-standard | General knowledge |
---
### Music/Rhythm (20 presets)
Beat-matching and rhythm games with vibrant audio-visual experiences.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| rhythm-disco-01 | Disco Fever | Playful, 8 | colorful/disco | platformer-standard | Disco rhythm |
| rhythm-disco-02 | Dance Floor | Intense, 8 | colorful/neon | platformer-standard | Dancing games |
| rhythm-retro-01 | Arcade Beats | Playful, 7 | retro/arcadeCabinet | platformer-standard | Retro rhythm |
| rhythm-retro-02 | Pixel Grooves | Quirky, 6 | retro/pixelGrid | platformer-standard | 8-bit music |
| rhythm-concert-01 | Rock Concert | Epic, 9 | colorful/neon | superhero-cape | Rock rhythm |
| rhythm-concert-02 | Stage Star | Heroic, 8 | colorful/disco | platformer-standard | Concert games |
| rhythm-space-01 | Cosmic Beats | Ambient, 6 | space/nebula | space-suit | Space rhythm |
| rhythm-space-02 | Stellar Symphony | Mysterious, 5 | space/deepSpace | space-suit | Ambient music |
| rhythm-street-01 | Street Beat | Intense, 8 | urban/street | ninja-outfit | Hip-hop rhythm |
| rhythm-street-02 | Urban Flow | Heroic, 7 | urban/neonDistrict | platformer-standard | Street dancing |
| rhythm-tropical-01 | Island Vibes | Playful, 6 | seasonal/tropical | platformer-standard | Tropical rhythm |
| rhythm-tropical-02 | Beach Party | Playful, 7 | seasonal/summerBeach | platformer-standard | Beach music |
| rhythm-abstract-01 | Visual Beats | Ambient, 5 | abstract/waves | platformer-standard | Abstract rhythm |
| rhythm-abstract-02 | Synth Wave | Quirky, 6 | abstract/kaleidoscope | platformer-standard | Synth music |
| rhythm-classical-01 | Symphony Hall | Chill, 4 | indoor/library | wizard-robes | Classical music |
| rhythm-classical-02 | Piano Master | Ambient, 3 | indoor/arcade | platformer-standard | Piano rhythm |
| rhythm-festival-01 | Festival Fever | Epic, 9 | colorful/tieDye | platformer-standard | Festival music |
| rhythm-festival-02 | Rave Night | Intense, 10 | colorful/neon | ninja-outfit | Rave games |
| rhythm-jazz-01 | Jazz Lounge | Chill, 4 | urban/alley | platformer-standard | Jazz rhythm |
| rhythm-drums-01 | Drum Circle | Heroic, 7 | nature/jungle | adventure-hero | Drum games |
---
### Creative/Drawing (20 presets)
Art and creativity games encouraging self-expression.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| creative-studio-01 | Art Studio | Chill, 3 | indoor/classroom | platformer-standard | Drawing games |
| creative-studio-02 | Paint Paradise | Playful, 4 | colorful/paintSplatter | platformer-standard | Painting games |
| creative-nature-01 | Nature Sketch | Ambient, 3 | nature/meadow | adventure-hero | Nature drawing |
| creative-nature-02 | Forest Canvas | Chill, 2 | nature/forest | adventure-hero | Landscape art |
| creative-colorful-01 | Color Splash | Playful, 5 | colorful/rainbow | platformer-standard | Coloring games |
| creative-colorful-02 | Neon Art | Quirky, 6 | colorful/neon | platformer-standard | Glow art |
| creative-abstract-01 | Abstract Express | Ambient, 4 | abstract/gradients | platformer-standard | Abstract art |
| creative-abstract-02 | Shape Maker | Chill, 3 | abstract/geometricPatterns | platformer-standard | Geometric art |
| creative-kids-01 | Crayon World | Playful, 5 | colorful/candyWorld | platformer-standard | Kids drawing |
| creative-kids-02 | Doodle Town | Playful, 6 | colorful/paintSplatter | platformer-standard | Doodling games |
| creative-fantasy-01 | Dragon Painter | Heroic, 5 | fantasy/enchantedForest | wizard-robes | Fantasy art |
| creative-fantasy-02 | Magic Canvas | Mysterious, 4 | fantasy/magicalLibrary | wizard-robes | Magical drawing |
| creative-space-01 | Galaxy Art | Ambient, 3 | space/nebula | space-suit | Space art |
| creative-space-02 | Cosmic Creator | Mysterious, 4 | space/deepSpace | space-suit | Universe creation |
| creative-beach-01 | Beach Doodles | Chill, 3 | seasonal/summerBeach | platformer-standard | Beach art |
| creative-beach-02 | Sand Art | Ambient, 2 | nature/desert | platformer-standard | Sand drawing |
| creative-city-01 | City Sketch | Quirky, 4 | urban/citySkyline | platformer-standard | Urban art |
| creative-city-02 | Street Art | Playful, 5 | urban/alley | ninja-outfit | Graffiti games |
| creative-pixel-01 | Pixel Art | Quirky, 5 | retro/pixelGrid | platformer-standard | Pixel drawing |
| creative-food-01 | Food Art | Playful, 4 | indoor/kitchen | chef-outfit | Food decoration |
---
### RPG/Battle (20 presets)
Role-playing and combat games with epic adventures.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| rpg-fantasy-01 | Kingdom Quest | Epic, 8 | fantasy/castle | knight-armor | Fantasy RPGs |
| rpg-fantasy-02 | Hero's Journey | Heroic, 7 | fantasy/enchantedForest | adventure-hero | Adventure RPGs |
| rpg-wizard-01 | Arcane Academy | Mysterious, 6 | fantasy/magicalLibrary | wizard-robes | Magic RPGs |
| rpg-wizard-02 | Sorcerer Supreme | Epic, 8 | fantasy/tower | wizard-robes | Wizard battles |
| rpg-ninja-01 | Ninja Chronicles | Intense, 7 | fantasy/temple | ninja-outfit | Ninja RPGs |
| rpg-ninja-02 | Shadow Warrior | Mysterious, 8 | spooky/darkForest | ninja-outfit | Stealth RPGs |
| rpg-dragon-01 | Dragon Rider | Epic, 9 | sky/flyingHigh | dragon-rider | Dragon games |
| rpg-dragon-02 | Dragon Slayer | Heroic, 9 | fantasy/floatingIslands | knight-armor | Dragon battles |
| rpg-dungeon-01 | Dungeon Crawler | Mysterious, 7 | fantasy/dungeon | knight-armor | Dungeon games |
| rpg-dungeon-02 | Crypt Explorer | Mysterious, 6 | spooky/crypt | adventure-hero | Crypt exploration |
| rpg-spooky-01 | Ghost Hunter | Mysterious, 6 | spooky/hauntedHouse | adventure-hero | Ghost hunting |
| rpg-spooky-02 | Vampire Slayer | Intense, 8 | spooky/manor | knight-armor | Monster hunting |
| rpg-nature-01 | Forest Guardian | Heroic, 6 | nature/forest | adventure-hero | Nature RPGs |
| rpg-nature-02 | Wilderness Hero | Playful, 5 | nature/meadow | adventure-hero | Exploration RPGs |
| rpg-arena-01 | Battle Arena | Epic, 9 | sports/arena | knight-armor | Arena battles |
| rpg-arena-02 | Gladiator Glory | Intense, 9 | sports/stadium | superhero-cape | Gladiator games |
| rpg-ice-01 | Frost Warrior | Heroic, 7 | seasonal/winterWonderland | knight-armor | Ice RPGs |
| rpg-ice-02 | Blizzard Quest | Intense, 8 | weather/snow | adventure-hero | Winter quests |
| rpg-volcano-01 | Lava Lord | Epic, 9 | nature/mountains | knight-armor | Volcano adventures |
| rpg-boss-01 | Boss Battle | Epic, 10 | fantasy/dungeon | superhero-cape | Boss fights |
---
### Sports (20 presets)
Athletic competition games across various sports.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| sports-soccer-01 | Soccer Star | Heroic, 8 | sports/field | sports-player | Soccer games |
| sports-soccer-02 | World Cup | Epic, 9 | sports/stadium | sports-player | Soccer tournaments |
| sports-basketball-01 | Court Kings | Heroic, 8 | sports/court | sports-player | Basketball games |
| sports-basketball-02 | Slam Dunk | Epic, 9 | sports/arena | sports-player | Basketball action |
| sports-baseball-01 | Home Run Hero | Heroic, 7 | sports/field | sports-player | Baseball games |
| sports-baseball-02 | World Series | Epic, 8 | sports/stadium | sports-player | Baseball tournaments |
| sports-tennis-01 | Tennis Ace | Playful, 6 | sports/court | sports-player | Tennis games |
| sports-tennis-02 | Grand Slam | Heroic, 7 | sports/stadium | sports-player | Tennis tournaments |
| sports-golf-01 | Golf Master | Chill, 4 | nature/meadow | sports-player | Golf games |
| sports-golf-02 | Championship Green | Ambient, 5 | sports/field | sports-player | Golf tournaments |
| sports-swimming-01 | Pool Champion | Heroic, 6 | sports/pool | sports-player | Swimming races |
| sports-swimming-02 | Dive Star | Playful, 5 | underwater/coralReef | sports-player | Diving games |
| sports-football-01 | Touchdown Hero | Epic, 8 | sports/field | sports-player | Football games |
| sports-football-02 | Super Bowl | Epic, 9 | sports/stadium | sports-player | Football tournaments |
| sports-hockey-01 | Ice Champion | Intense, 8 | sports/arena | sports-player | Hockey games |
| sports-hockey-02 | Puck Master | Heroic, 7 | seasonal/winterWonderland | sports-player | Ice hockey |
| sports-boxing-01 | Ring Champion | Intense, 9 | sports/arena | superhero-cape | Boxing games |
| sports-boxing-02 | Knockout King | Epic, 9 | sports/gym | superhero-cape | Fighting games |
| sports-extreme-01 | Extreme Games | Intense, 8 | nature/mountains | adventure-hero | Extreme sports |
| sports-extreme-02 | X-Games Hero | Heroic, 9 | sports/skatepark | adventure-hero | Skateboarding |
---
### Physics/Pinball (20 presets)
Physics-based puzzle games and pinball simulations.
| ID | Name | Music | Background | Context | Best For |
|----|------|-------|------------|---------|----------|
| physics-pinball-01 | Classic Pinball | Playful, 7 | retro/arcadeCabinet | platformer-standard | Classic pinball |
| physics-pinball-02 | Neon Pinball | Intense, 8 | colorful/neon | platformer-standard | Modern pinball |
| physics-retro-01 | Retro Physics | Quirky, 6 | retro/pixelGrid | platformer-standard | Retro physics |
| physics-retro-02 | Arcade Physics | Playful, 7 | retro/arcadeCabinet | platformer-standard | Arcade physics |
| physics-mechanical-01 | Gear Works | Quirky, 5 | mechanical/gears | scientist-labcoat | Gear puzzles |
| physics-mechanical-02 | Factory Fun | Quirky, 6 | mechanical/factory | scientist-labcoat | Factory games |
| physics-colorful-01 | Bouncy World | Playful, 6 | colorful/rainbow | platformer-standard | Bouncing games |
| physics-colorful-02 | Candy Physics | Playful, 5 | colorful/candyWorld | platformer-standard | Candy physics |
| physics-gravity-01 | Zero Gravity | Ambient, 4 | space/deepSpace | space-suit | Gravity puzzles |
| physics-gravity-02 | Orbital Mechanics | Mysterious, 5 | space/planets | space-suit | Space physics |
| physics-destruction-01 | Demolition Derby | Intense, 8 | urban/citySkyline | superhero-cape | Destruction games |
| physics-destruction-02 | Crash Test | Heroic, 7 | mechanical/factory | platformer-standard | Crash physics |
| physics-water-01 | Water Works | Chill, 4 | underwater/coralReef | platformer-standard | Water physics |
| physics-water-02 | Fluid Dynamics | Ambient, 3 | abstract/waves | scientist-labcoat | Fluid games |
| physics-balance-01 | Stack Master | Quirky, 5 | abstract/minimalist | platformer-standard | Stacking games |
| physics-balance-02 | Tower Builder | Playful, 6 | urban/citySkyline | platformer-standard | Building games |
| physics-launch-01 | Launcher Pro | Heroic, 7 | sky/clouds | platformer-standard | Launch games |
| physics-launch-02 | Catapult King | Epic, 8 | fantasy/castle | knight-armor | Siege games |
| physics-pendulum-01 | Pendulum Puzzle | Chill, 4 | abstract/geometricPatterns | scientist-labcoat | Pendulum games |
| physics-rube-01 | Rube Goldberg | Quirky, 6 | mechanical/conveyorBelts | scientist-labcoat | Chain reaction |
---
## Using Presets
### Get Preset by ID
```javascript
const preset = AssetPresets.getById('action-space-01');
// Returns: { id, name, music, background, avatarContext, ... }
```
### Get All for Game Style
```javascript
const actionPresets = AssetPresets.getByGameStyle('action-arcade');
// Returns array of 20 presets
```
### Get Random Preset
```javascript
const random = AssetPresets.getRandom('racing');
// Returns single random preset from racing category
```
### Search Presets
```javascript
const matches = AssetPresets.search('dragon');
// Returns all presets with "dragon" in name, description, or tags
```
### Load Preset
```javascript
const preset = AssetPresets.getById('action-space-01');
const config = {
music: preset.music,
background: preset.background,
avatarContext: preset.avatarContext
};
await AssetManager.loadGameAssets(config);
```
---
## Customizing Presets
### Override Music
Keep the visual theme but change the music mood:
```javascript
const preset = AssetPresets.getById('action-space-01');
const custom = {
...preset,
music: { mood: 'Mysterious', energy: 5 } // Different mood
};
```
### Keep Music, Change Background
Swap the background while maintaining audio coherence:
```javascript
const preset = AssetPresets.getById('puzzle-zen-01');
const custom = {
music: preset.music,
background: { theme: 'underwater', variant: 'coralReef' },
avatarContext: preset.avatarContext
};
```
### Change Avatar Context Only
Different character presentation with same environment:
```javascript
const preset = AssetPresets.getById('racing-street-01');
const custom = {
...preset,
avatarContext: { mode: 'FULL_BODY', context: 'motorcycle' }
};
```
---
## Preset Variety Strategy
For maximum variety in generated games:
### 1. Track Recently Used Presets
```javascript
const usedPresets = new Set();
function getUnusedPreset(gameStyle) {
const presets = AssetPresets.getByGameStyle(gameStyle);
const unused = presets.filter(p => !usedPresets.has(p.id));
if (unused.length === 0) {
usedPresets.clear(); // Reset when all used
return presets[0];
}
const selected = unused[Math.floor(Math.random() * unused.length)];
usedPresets.add(selected.id);
return selected;
}
```
### 2. Cycle Through All 20 Presets
```javascript
const presetIndex = {};
function getNextPreset(gameStyle) {
if (!presetIndex[gameStyle]) presetIndex[gameStyle] = 0;
const presets = AssetPresets.getByGameStyle(gameStyle);
const preset = presets[presetIndex[gameStyle]];
presetIndex[gameStyle] = (presetIndex[gameStyle] + 1) % presets.length;
return preset;
}
```
### 3. Use Variation Functions
```javascript
// Get variation of a preset
const variation = AssetManager.getVariation(preset, 'background');
// Same music and context, different background variant
```
---
## Validation
All presets are pre-validated for compatibility. To check custom modifications:
```javascript
const result = AssetCompatibility.validate(customConfig);
if (result.score < 70) {
console.warn('Low compatibility score:', result.score);
console.warn('Issues:', result.issues);
}
// result.score: 0-100 compatibility score
// result.issues: array of compatibility warnings
```
### Compatibility Criteria
| Score Range | Quality |
|-------------|---------|
| 90-100 | Excellent - perfect harmony |
| 70-89 | Good - works well |
| 50-69 | Fair - noticeable mismatch |
| 0-49 | Poor - significant issues |
---
## Quick Reference
### Music Moods
| Mood | Energy Range | Best For |
|------|--------------|----------|
| Epic | 7-10 | Boss fights, climactic moments |
| Heroic | 6-9 | Adventures, triumphs |
| Intense | 8-10 | Chases, action sequences |
| Playful | 4-7 | Fun, lighthearted games |
| Mysterious | 2-5 | Puzzles, suspense |
| Ambient | 1-3 | Calm, atmospheric |
| Chill | 1-4 | Relaxation, menus |
| Quirky | 4-8 | Comedy, eccentric |
### Background Themes
15 themes with 8 variants each (120 total):
- **space**: nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole
- **nature**: forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow
- **urban**: citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley
- **fantasy**: castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower
- **abstract**: geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix
- **retro**: pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave
- **indoor**: laboratory, classroom, bedroom, kitchen, arcade, office, library, gym
- **weather**: rain, snow, storm, sunset, sunrise, fog, lightning, aurora
- **sports**: stadium, raceTrack, court, field, arena, pool, gym, skatepark
- **seasonal**: springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical
- **underwater**: coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure
- **sky**: clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield
- **mechanical**: gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab
- **spooky**: hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows
- **colorful**: rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism
### Avatar Contexts
| Category | Contexts |
|----------|----------|
| **Vehicles** | spaceship-cockpit, race-car, airplane, flappy-style, tank, pacman-style, hoverboard, jetpack, mech-suit, submarine, dragon-rider, motorcycle |
| **Costumes** | knight-armor, space-suit, ninja-outfit, wizard-robes, pirate-outfit, superhero-cape, adventure-hero, scientist-labcoat, chef-outfit, sports-player, cowboy-gear |
| **Pure** | platformer-standard, runner |
---
## See Also
- [Asset Library Overview](./ASSET_LIBRARY_OVERVIEW.md) - Complete asset system documentation
- [Adding New Assets](./ADDING_NEW_ASSETS.md) - How to create new assets
- [Game Generation Guide](./GAME_GENERATION_GUIDE.md) - How assets are used in generation
@@ -0,0 +1,675 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Asset Comparison Demo - Same Game, Different Assets</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f1a;
color: #e2e8f0;
min-height: 100vh;
}
.header {
background: #1a1a2e;
padding: 1.5rem 2rem;
border-bottom: 1px solid #334155;
}
.header h1 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.header p {
color: #94a3b8;
font-size: 0.9rem;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
@media (max-width: 1200px) {
.grid { grid-template-columns: 1fr; }
}
.game-panel {
background: #1a1a2e;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid #334155;
}
.panel-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #334155;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h2 {
font-size: 1.125rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.badge {
padding: 0.25rem 0.75rem;
background: #6366f1;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.badge.space { background: #3b82f6; }
.badge.nature { background: #22c55e; }
.badge.spooky { background: #8b5cf6; }
.badge.retro { background: #f59e0b; }
.game-frame {
position: relative;
width: 100%;
aspect-ratio: 16/10;
background: #0f0f1a;
}
.game-frame canvas {
width: 100%;
height: 100%;
}
.panel-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #334155;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
padding: 0.25rem 0.5rem;
background: #252542;
border-radius: 0.25rem;
font-size: 0.75rem;
color: #94a3b8;
}
.controls {
margin-top: 2rem;
padding: 1.5rem;
background: #1a1a2e;
border-radius: 0.75rem;
border: 1px solid #334155;
}
.controls h3 {
margin-bottom: 1rem;
}
.control-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.btn {
padding: 0.625rem 1.25rem;
background: #6366f1;
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.875rem;
transition: background 0.2s;
}
.btn:hover {
background: #818cf8;
}
.btn.secondary {
background: #334155;
}
.btn.secondary:hover {
background: #475569;
}
.code-section {
margin-top: 2rem;
}
.code-section h3 {
margin-bottom: 1rem;
}
.code-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.code-tab {
padding: 0.5rem 1rem;
background: #252542;
border: none;
color: #94a3b8;
border-radius: 0.375rem;
cursor: pointer;
}
.code-tab.active {
background: #6366f1;
color: white;
}
.code-block {
background: #0f0f1a;
border: 1px solid #334155;
border-radius: 0.5rem;
padding: 1rem;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.75rem;
overflow-x: auto;
white-space: pre;
max-height: 300px;
overflow-y: auto;
}
.instructions {
margin-top: 2rem;
padding: 1.5rem;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 0.75rem;
}
.instructions h3 {
margin-bottom: 0.5rem;
}
.instructions p {
opacity: 0.9;
font-size: 0.9rem;
line-height: 1.6;
}
</style>
</head>
<body>
<header class="header">
<h1>Asset Comparison Demo</h1>
<p>The same dodge-the-obstacles game rendered with 4 different asset combinations</p>
</header>
<div class="container">
<div class="grid">
<!-- Space Theme -->
<div class="game-panel">
<div class="panel-header">
<h2><span>Space Dodger</span></h2>
<span class="badge space">Space Theme</span>
</div>
<div class="game-frame">
<canvas id="game1" width="640" height="400"></canvas>
</div>
<div class="panel-footer">
<span class="tag">space/nebula</span>
<span class="tag">Epic music</span>
<span class="tag">spaceship-cockpit</span>
<span class="tag">HEAD_ONLY</span>
</div>
</div>
<!-- Nature Theme -->
<div class="game-panel">
<div class="panel-header">
<h2><span>Forest Runner</span></h2>
<span class="badge nature">Nature Theme</span>
</div>
<div class="game-frame">
<canvas id="game2" width="640" height="400"></canvas>
</div>
<div class="panel-footer">
<span class="tag">nature/forest</span>
<span class="tag">Playful music</span>
<span class="tag">platformer-standard</span>
<span class="tag">FULL_BODY</span>
</div>
</div>
<!-- Spooky Theme -->
<div class="game-panel">
<div class="panel-header">
<h2><span>Haunted Escape</span></h2>
<span class="badge spooky">Spooky Theme</span>
</div>
<div class="game-frame">
<canvas id="game3" width="640" height="400"></canvas>
</div>
<div class="panel-footer">
<span class="tag">spooky/hauntedHouse</span>
<span class="tag">Mysterious music</span>
<span class="tag">adventure-hero</span>
<span class="tag">FULL_BODY</span>
</div>
</div>
<!-- Retro Theme -->
<div class="game-panel">
<div class="panel-header">
<h2><span>Pixel Dash</span></h2>
<span class="badge retro">Retro Theme</span>
</div>
<div class="game-frame">
<canvas id="game4" width="640" height="400"></canvas>
</div>
<div class="panel-footer">
<span class="tag">retro/synthwave</span>
<span class="tag">Quirky music</span>
<span class="tag">runner</span>
<span class="tag">FULL_BODY</span>
</div>
</div>
</div>
<div class="controls">
<h3>Global Controls</h3>
<div class="control-row">
<button class="btn" onclick="startAll()">Start All Games</button>
<button class="btn secondary" onclick="pauseAll()">Pause All</button>
<button class="btn secondary" onclick="resetAll()">Reset All</button>
</div>
</div>
<div class="instructions">
<h3>How to Play</h3>
<p>
Use <strong>Arrow Keys</strong> or <strong>WASD</strong> to move. Avoid the obstacles!
Each panel shows the same game logic with different visual assets.
Notice how the background, music mood, avatar context, and rendering mode
completely change the feel of the game while the core mechanics remain identical.
</p>
</div>
<div class="code-section">
<h3>Configuration Code</h3>
<div class="code-tabs">
<button class="code-tab active" onclick="showCode('space')">Space</button>
<button class="code-tab" onclick="showCode('nature')">Nature</button>
<button class="code-tab" onclick="showCode('spooky')">Spooky</button>
<button class="code-tab" onclick="showCode('retro')">Retro</button>
</div>
<div class="code-block" id="codeDisplay">const GAME_CONFIG = {
title: "Space Dodger",
assets: {
music: { mood: "Epic", energy: 8, enabled: true },
background: { theme: "space", variant: "nebula", animated: true },
avatarContext: {
mode: "HEAD_ONLY",
context: "spaceship-cockpit",
showAccessories: true
}
}
};</div>
</div>
</div>
<!-- Asset Libraries -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/space.js"></script>
<script src="/assets/backgrounds/themes/nature.js"></script>
<script src="/assets/backgrounds/themes/spooky.js"></script>
<script src="/assets/backgrounds/themes/retro.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script src="/assets/avatar/contexts/vehicles.js"></script>
<script src="/assets/avatar/contexts/pure.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<script>
// ═══════════════════════════════════════════════════════════════════════
// COMPARISON DEMO - Same game, 4 different asset sets
// ═══════════════════════════════════════════════════════════════════════
const CONFIGS = {
space: {
title: "Space Dodger",
assets: {
music: { mood: "Epic", energy: 8 },
background: { theme: "space", variant: "nebula" },
avatar: { mode: "HEAD_ONLY", context: "spaceship-cockpit" }
},
colors: {
player: '#3b82f6',
obstacle: '#ef4444',
coin: '#fbbf24'
}
},
nature: {
title: "Forest Runner",
assets: {
music: { mood: "Playful", energy: 6 },
background: { theme: "nature", variant: "forest" },
avatar: { mode: "FULL_BODY", context: "platformer-standard" }
},
colors: {
player: '#22c55e',
obstacle: '#a16207',
coin: '#fbbf24'
}
},
spooky: {
title: "Haunted Escape",
assets: {
music: { mood: "Mysterious", energy: 5 },
background: { theme: "spooky", variant: "hauntedHouse" },
avatar: { mode: "FULL_BODY", context: "adventure-hero" }
},
colors: {
player: '#a855f7',
obstacle: '#7c3aed',
coin: '#c084fc'
}
},
retro: {
title: "Pixel Dash",
assets: {
music: { mood: "Quirky", energy: 7 },
background: { theme: "retro", variant: "synthwave" },
avatar: { mode: "FULL_BODY", context: "runner" }
},
colors: {
player: '#f59e0b',
obstacle: '#ec4899',
coin: '#06b6d4'
}
}
};
const playerAvatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59'
};
// Game instances
const games = {};
const keys = {};
// Input handling
document.addEventListener('keydown', e => { keys[e.code] = true; });
document.addEventListener('keyup', e => { keys[e.code] = false; });
// ═══════════════════════════════════════════════════════════════════════
// GAME CLASS
// ═══════════════════════════════════════════════════════════════════════
class DodgeGame {
constructor(canvasId, config) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.config = config;
this.reset();
this.running = false;
this.lastTime = 0;
}
reset() {
this.playerX = this.canvas.width / 2;
this.playerY = this.canvas.height - 80;
this.score = 0;
this.obstacles = [];
this.coins = [];
this.animTime = 0;
this.spawnTimer = 0;
this.coinTimer = 0;
}
start() {
this.running = true;
this.lastTime = performance.now();
this.loop();
}
pause() {
this.running = false;
}
loop() {
if (!this.running) return;
const now = performance.now();
const dt = Math.min((now - this.lastTime) / 1000, 0.05);
this.lastTime = now;
this.update(dt);
this.render();
requestAnimationFrame(() => this.loop());
}
update(dt) {
const speed = 200;
// Player movement
if (keys['ArrowLeft'] || keys['KeyA']) {
this.playerX = Math.max(30, this.playerX - speed * dt);
}
if (keys['ArrowRight'] || keys['KeyD']) {
this.playerX = Math.min(this.canvas.width - 30, this.playerX + speed * dt);
}
// Spawn obstacles
this.spawnTimer += dt;
if (this.spawnTimer > 1.5) {
this.spawnTimer = 0;
this.obstacles.push({
x: 50 + Math.random() * (this.canvas.width - 100),
y: -30,
size: 20 + Math.random() * 20
});
}
// Spawn coins
this.coinTimer += dt;
if (this.coinTimer > 2) {
this.coinTimer = 0;
this.coins.push({
x: 50 + Math.random() * (this.canvas.width - 100),
y: -20
});
}
// Update obstacles
this.obstacles = this.obstacles.filter(o => {
o.y += 150 * dt;
// Collision
const dx = this.playerX - o.x;
const dy = this.playerY - o.y;
if (Math.sqrt(dx*dx + dy*dy) < o.size + 25) {
this.reset();
return false;
}
return o.y < this.canvas.height + 50;
});
// Update coins
this.coins = this.coins.filter(c => {
c.y += 120 * dt;
// Collection
const dx = this.playerX - c.x;
const dy = this.playerY - c.y;
if (Math.sqrt(dx*dx + dy*dy) < 35) {
this.score += 10;
return false;
}
return c.y < this.canvas.height + 30;
});
this.score += Math.floor(dt * 5);
this.animTime += dt * 1000;
}
render() {
const { ctx, canvas, config } = this;
// Background
if (window.BackgroundEngine) {
window.BackgroundEngine.render(
ctx,
config.assets.background.theme,
config.assets.background.variant,
canvas.width,
canvas.height,
this.animTime
);
} else {
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Obstacles
ctx.fillStyle = config.colors.obstacle;
this.obstacles.forEach(o => {
ctx.beginPath();
ctx.moveTo(o.x, o.y - o.size);
ctx.lineTo(o.x + o.size, o.y + o.size);
ctx.lineTo(o.x - o.size, o.y + o.size);
ctx.closePath();
ctx.fill();
});
// Coins
ctx.fillStyle = config.colors.coin;
this.coins.forEach(c => {
ctx.beginPath();
ctx.arc(c.x, c.y, 12, 0, Math.PI * 2);
ctx.fill();
});
// Player
ctx.save();
ctx.translate(this.playerX, this.playerY);
const mode = config.assets.avatar.mode;
const dims = {
'HEAD_ONLY': { w: 64, h: 64, offsetY: 32 },
'HEAD_AND_SHOULDERS': { w: 64, h: 96, offsetY: 48 },
'UPPER_BODY': { w: 64, h: 128, offsetY: 64 },
'FULL_BODY': { w: 64, h: 160, offsetY: 80 }
};
const d = dims[mode];
if (window.AvatarSystem) {
ctx.translate(-32, -d.offsetY);
window.AvatarSystem.render(
ctx,
playerAvatar,
mode,
'idle',
this.animTime
);
} else {
// Fallback
ctx.fillStyle = config.colors.player;
ctx.beginPath();
ctx.arc(0, 0, 25, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// Score
ctx.fillStyle = 'white';
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${this.score}`, 10, 25);
}
}
// ═══════════════════════════════════════════════════════════════════════
// CONTROLS
// ═══════════════════════════════════════════════════════════════════════
function startAll() {
Object.values(games).forEach(g => g.start());
}
function pauseAll() {
Object.values(games).forEach(g => g.pause());
}
function resetAll() {
Object.values(games).forEach(g => {
g.reset();
g.render();
});
}
function showCode(theme) {
document.querySelectorAll('.code-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
const config = CONFIGS[theme];
const code = `const GAME_CONFIG = {
title: "${config.title}",
assets: {
music: { mood: "${config.assets.music.mood}", energy: ${config.assets.music.energy}, enabled: true },
background: { theme: "${config.assets.background.theme}", variant: "${config.assets.background.variant}", animated: true },
avatarContext: {
mode: "${config.assets.avatar.mode}",
context: "${config.assets.avatar.context}",
showAccessories: true
}
}
};`;
document.getElementById('codeDisplay').textContent = code;
}
// ═══════════════════════════════════════════════════════════════════════
// INITIALIZE
// ═══════════════════════════════════════════════════════════════════════
window.addEventListener('load', () => {
// Create game instances
games.space = new DodgeGame('game1', CONFIGS.space);
games.nature = new DodgeGame('game2', CONFIGS.nature);
games.spooky = new DodgeGame('game3', CONFIGS.spooky);
games.retro = new DodgeGame('game4', CONFIGS.retro);
// Initial render
Object.values(games).forEach(g => g.render());
// Auto-start after a moment
setTimeout(startAll, 500);
});
</script>
</body>
</html>
@@ -0,0 +1,533 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Sky Glide - Flappy Example</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; overflow: hidden; background: #1a1a2e; touch-action: none; }
canvas { display: block; }
#ui {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
color: white;
font-family: Arial, sans-serif;
font-size: 36px;
font-weight: bold;
text-shadow: 2px 2px 4px #000;
z-index: 10;
}
#loading {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, #87CEEB, #4682B4);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-family: Arial, sans-serif;
z-index: 100;
}
#loading h1 { margin-bottom: 1rem; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
.loading-bar {
width: 200px;
height: 8px;
background: rgba(255,255,255,0.3);
border-radius: 4px;
overflow: hidden;
}
.loading-bar-fill {
height: 100%;
background: white;
width: 0%;
transition: width 0.3s;
}
#startScreen {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-family: Arial, sans-serif;
z-index: 50;
}
#startScreen h2 { font-size: 2rem; margin-bottom: 1rem; }
#startScreen p { font-size: 1.25rem; opacity: 0.9; }
</style>
</head>
<body>
<div id="loading">
<h1>Sky Glide</h1>
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
<p id="loadingText">Loading assets...</p>
</div>
<div id="startScreen">
<h2>Tap or Press Space to Start</h2>
<p>Avoid the pipes!</p>
</div>
<canvas id="gameCanvas"></canvas>
<div id="ui"><span id="score">0</span></div>
<!-- Asset Libraries -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/sky.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/music/musicEngine.js"></script>
<script src="/assets/music/library/songs1-80.js"></script>
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script src="/assets/avatar/contexts/vehicles.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<script>
// ═══════════════════════════════════════════════════════════════════════
// FLAPPY EXAMPLE - Head Only Avatar Demo
// ═══════════════════════════════════════════════════════════════════════
// Demonstrates: HEAD_ONLY avatar, flappy-style context,
// sky/clouds background, Playful music mood
const GAME_CONFIG = {
title: "Sky Glide",
assets: {
music: { mood: "Playful", energy: 5, enabled: true },
background: { theme: "sky", variant: "clouds", animated: true },
avatarContext: { mode: "HEAD_ONLY", context: "flappy-style", showAccessories: true }
},
gameplay: {
gravity: 1200,
flapForce: 400,
pipeSpeed: 200,
pipeGap: 180,
pipeWidth: 70,
pipeSpawnRate: 1800
}
};
const playerAvatar = {
faceShape: 'round',
skinTone: 'light',
hairStyle: 'spiky',
hairColor: '#FFD700',
eyeShape: 'round',
eyeColor: '#3498db',
noseShape: 'small',
mouthShape: 'smile'
};
// ═══════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
let state = {
playerY: 0,
playerVelY: 0,
playerRotation: 0,
animTime: 0,
score: 0,
highScore: parseInt(localStorage.getItem('skyglide_high') || '0'),
gameOver: false,
started: false,
paused: false,
lastTime: 0,
pipes: [],
lastPipeTime: 0,
clouds: []
};
// ═══════════════════════════════════════════════════════════════════════
// LOADING
// ═══════════════════════════════════════════════════════════════════════
function updateLoading(progress, text) {
document.getElementById('loadingFill').style.width = progress + '%';
document.getElementById('loadingText').textContent = text;
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
document.getElementById('startScreen').style.display = 'flex';
}
async function loadAssets() {
updateLoading(25, 'Loading sky...');
await new Promise(r => setTimeout(r, 200));
updateLoading(50, 'Loading music...');
await new Promise(r => setTimeout(r, 200));
updateLoading(75, 'Loading avatar...');
await new Promise(r => setTimeout(r, 200));
updateLoading(100, 'Ready!');
await new Promise(r => setTimeout(r, 300));
hideLoading();
initGame();
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
// ═══════════════════════════════════════════════════════════════════════
// GAME LOGIC
// ═══════════════════════════════════════════════════════════════════════
function initGame() {
state.playerY = canvas.height / 2;
state.playerVelY = 0;
state.playerRotation = 0;
state.pipes = [];
state.lastPipeTime = 0;
state.score = 0;
state.gameOver = false;
updateScoreDisplay();
// Initialize clouds for parallax
state.clouds = [];
for (let i = 0; i < 5; i++) {
state.clouds.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height * 0.6,
size: 50 + Math.random() * 100,
speed: 20 + Math.random() * 30
});
}
}
function flap() {
if (state.gameOver) {
state.score = 0;
state.gameOver = false;
state.started = false;
document.getElementById('startScreen').style.display = 'flex';
initGame();
return;
}
if (!state.started) {
state.started = true;
document.getElementById('startScreen').style.display = 'none';
// Start music
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
try {
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
} catch(e) {}
}
}
state.playerVelY = -GAME_CONFIG.gameplay.flapForce;
}
function updateGame(dt) {
if (!state.started || state.gameOver) {
// Bob animation when not playing
state.playerY = canvas.height / 2 + Math.sin(state.animTime / 300) * 20;
state.animTime += dt * 1000;
return;
}
const { gravity, pipeSpeed, pipeGap, pipeWidth, pipeSpawnRate } = GAME_CONFIG.gameplay;
// Apply gravity
state.playerVelY += gravity * dt;
state.playerY += state.playerVelY * dt;
// Rotation based on velocity
state.playerRotation = Math.min(Math.max(state.playerVelY / 10, -30), 90);
// Spawn pipes
state.lastPipeTime += dt * 1000;
if (state.lastPipeTime > pipeSpawnRate) {
state.lastPipeTime = 0;
const gapY = 150 + Math.random() * (canvas.height - 350);
state.pipes.push({
x: canvas.width + pipeWidth,
gapY: gapY,
passed: false
});
}
// Update pipes
state.pipes = state.pipes.filter(pipe => {
pipe.x -= pipeSpeed * dt;
// Score when passing
if (!pipe.passed && pipe.x + pipeWidth < canvas.width / 3) {
pipe.passed = true;
state.score++;
updateScoreDisplay();
}
return pipe.x > -pipeWidth;
});
// Update clouds
state.clouds.forEach(cloud => {
cloud.x -= cloud.speed * dt;
if (cloud.x + cloud.size < 0) {
cloud.x = canvas.width + cloud.size;
cloud.y = Math.random() * canvas.height * 0.6;
}
});
// Collision detection
const playerX = canvas.width / 3;
const playerSize = 32;
// Ground/ceiling
if (state.playerY < playerSize || state.playerY > canvas.height - playerSize) {
endGame();
return;
}
// Pipes
state.pipes.forEach(pipe => {
const pipeLeft = pipe.x;
const pipeRight = pipe.x + pipeWidth;
if (playerX + playerSize > pipeLeft && playerX - playerSize < pipeRight) {
// Check top pipe
if (state.playerY - playerSize < pipe.gapY - pipeGap / 2) {
endGame();
}
// Check bottom pipe
if (state.playerY + playerSize > pipe.gapY + pipeGap / 2) {
endGame();
}
}
});
state.animTime += dt * 1000;
}
function renderGame() {
// Sky gradient background
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#87CEEB');
gradient.addColorStop(0.5, '#B0E0E6');
gradient.addColorStop(1, '#E0F7FA');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Clouds (parallax)
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
state.clouds.forEach(cloud => {
drawCloud(cloud.x, cloud.y, cloud.size);
});
// Pipes
state.pipes.forEach(pipe => {
const { pipeWidth, pipeGap } = GAME_CONFIG.gameplay;
const topHeight = pipe.gapY - pipeGap / 2;
const bottomY = pipe.gapY + pipeGap / 2;
// Top pipe
ctx.fillStyle = '#2ECC71';
ctx.fillRect(pipe.x, 0, pipeWidth, topHeight);
ctx.fillStyle = '#27AE60';
ctx.fillRect(pipe.x - 5, topHeight - 30, pipeWidth + 10, 30);
// Bottom pipe
ctx.fillStyle = '#2ECC71';
ctx.fillRect(pipe.x, bottomY, pipeWidth, canvas.height - bottomY);
ctx.fillStyle = '#27AE60';
ctx.fillRect(pipe.x - 5, bottomY, pipeWidth + 10, 30);
// Pipe highlights
ctx.fillStyle = '#58D68D';
ctx.fillRect(pipe.x + 5, 0, 10, topHeight - 30);
ctx.fillRect(pipe.x + 5, bottomY + 30, 10, canvas.height - bottomY - 30);
});
// Ground
ctx.fillStyle = '#DEB887';
ctx.fillRect(0, canvas.height - 30, canvas.width, 30);
ctx.fillStyle = '#8B4513';
ctx.fillRect(0, canvas.height - 35, canvas.width, 5);
// Player
const playerX = canvas.width / 3;
ctx.save();
ctx.translate(playerX, state.playerY);
ctx.rotate(state.playerRotation * Math.PI / 180);
if (window.ContextRenderer) {
// Use context renderer for flappy style
ctx.translate(-40, -40);
window.ContextRenderer.render(
ctx,
GAME_CONFIG.assets.avatarContext.context,
playerAvatar,
40, 40,
'idle',
state.animTime
);
} else if (window.AvatarSystem) {
ctx.translate(-32, -32);
window.AvatarSystem.render(
ctx,
playerAvatar,
'HEAD_ONLY',
'idle',
state.animTime
);
} else {
// Fallback bird
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.ellipse(0, 0, 30, 25, 0, 0, Math.PI * 2);
ctx.fill();
// Eye
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(10, -5, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(12, -5, 5, 0, Math.PI * 2);
ctx.fill();
// Beak
ctx.fillStyle = '#FF6B00';
ctx.beginPath();
ctx.moveTo(25, 0);
ctx.lineTo(40, 5);
ctx.lineTo(25, 10);
ctx.closePath();
ctx.fill();
// Wing
ctx.fillStyle = '#FFA500';
ctx.beginPath();
ctx.ellipse(-5, 5, 15, 10, Math.sin(state.animTime / 50) * 0.3, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// Game over overlay
if (state.gameOver) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = 'bold 48px Arial';
ctx.textAlign = 'center';
ctx.fillText('Game Over', canvas.width / 2, canvas.height / 2 - 60);
ctx.font = '32px Arial';
ctx.fillText(`Score: ${state.score}`, canvas.width / 2, canvas.height / 2);
ctx.fillText(`Best: ${state.highScore}`, canvas.width / 2, canvas.height / 2 + 40);
ctx.font = '24px Arial';
ctx.fillText('Tap to Restart', canvas.width / 2, canvas.height / 2 + 100);
}
}
function drawCloud(x, y, size) {
ctx.beginPath();
ctx.arc(x, y, size * 0.5, 0, Math.PI * 2);
ctx.arc(x + size * 0.35, y - size * 0.15, size * 0.4, 0, Math.PI * 2);
ctx.arc(x + size * 0.6, y, size * 0.35, 0, Math.PI * 2);
ctx.arc(x + size * 0.3, y + size * 0.1, size * 0.3, 0, Math.PI * 2);
ctx.fill();
}
// ═══════════════════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════════════════
function updateScoreDisplay() {
document.getElementById('score').textContent = state.score;
}
function endGame() {
state.gameOver = true;
if (state.score > state.highScore) {
state.highScore = state.score;
localStorage.setItem('skyglide_high', state.highScore);
}
if (window.parent !== window) {
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
}
}
// ═══════════════════════════════════════════════════════════════════════
// INPUT
// ═══════════════════════════════════════════════════════════════════════
document.addEventListener('keydown', e => {
if (e.code === 'Space') {
e.preventDefault();
flap();
}
if (e.code === 'Escape') {
state.paused = !state.paused;
if (!state.paused) {
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
}
});
canvas.addEventListener('click', flap);
canvas.addEventListener('touchstart', e => {
e.preventDefault();
flap();
}, { passive: false });
// ═══════════════════════════════════════════════════════════════════════
// GAME LOOP
// ═══════════════════════════════════════════════════════════════════════
function gameLoop(time) {
if (state.paused) return;
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
state.lastTime = time;
updateGame(dt);
renderGame();
requestAnimationFrame(gameLoop);
}
window.pauseGame = () => { state.paused = true; };
window.resumeGame = () => {
state.paused = false;
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
};
loadAssets();
</script>
</body>
</html>
@@ -0,0 +1,533 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Forest Jumper - Platformer Example</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; overflow: hidden; background: #1a1a2e; touch-action: none; }
canvas { display: block; }
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: Arial, sans-serif;
font-size: 18px;
text-shadow: 2px 2px 4px #000;
z-index: 10;
}
#loading {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, #1a1a2e, #16213e);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-family: Arial, sans-serif;
z-index: 100;
}
#loading h1 { margin-bottom: 1rem; }
.loading-bar {
width: 200px;
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
}
.loading-bar-fill {
height: 100%;
background: #6366f1;
width: 0%;
transition: width 0.3s;
}
</style>
</head>
<body>
<div id="loading">
<h1>Forest Jumper</h1>
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
<p id="loadingText">Loading assets...</p>
</div>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div>Score: <span id="score">0</span></div>
<div>Coins: <span id="coins">0</span></div>
</div>
<!-- Asset Libraries -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/nature.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/music/musicEngine.js"></script>
<script src="/assets/music/library/songs1-80.js"></script>
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script src="/assets/avatar/contexts/pure.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<script>
// ═══════════════════════════════════════════════════════════════════════
// PLATFORMER EXAMPLE - Full Body Avatar Demo
// ═══════════════════════════════════════════════════════════════════════
// Demonstrates: FULL_BODY avatar, platformer-standard context,
// nature/forest background, Playful music mood
const GAME_CONFIG = {
title: "Forest Jumper",
assets: {
music: { mood: "Playful", energy: 6, enabled: true },
background: { theme: "nature", variant: "forest", animated: true },
avatarContext: { mode: "FULL_BODY", context: "platformer-standard", showAccessories: true }
},
gameplay: {
gravity: 1800,
jumpForce: 650,
moveSpeed: 280,
platformCount: 8,
coinSpawnRate: 2000
}
};
// Player avatar configuration
const playerAvatar = {
faceShape: 'round',
skinTone: 'medium',
hairStyle: 'short',
hairColor: '#3d2314',
eyeShape: 'round',
eyeColor: '#4a7c59',
noseShape: 'small',
mouthShape: 'smile'
};
// ═══════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
let state = {
playerX: 100,
playerY: 300,
playerVelX: 0,
playerVelY: 0,
isGrounded: false,
facingRight: true,
currentAnim: 'idle',
animTime: 0,
score: 0,
coins: 0,
gameOver: false,
paused: false,
lastTime: 0,
platforms: [],
collectedCoins: [],
particles: []
};
const keys = {};
// ═══════════════════════════════════════════════════════════════════════
// LOADING
// ═══════════════════════════════════════════════════════════════════════
let assetsLoaded = false;
function updateLoading(progress, text) {
document.getElementById('loadingFill').style.width = progress + '%';
document.getElementById('loadingText').textContent = text;
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
}
async function loadAssets() {
updateLoading(20, 'Loading backgrounds...');
await new Promise(r => setTimeout(r, 200));
updateLoading(40, 'Loading music...');
await new Promise(r => setTimeout(r, 200));
updateLoading(60, 'Loading avatars...');
await new Promise(r => setTimeout(r, 200));
updateLoading(80, 'Initializing game...');
await new Promise(r => setTimeout(r, 200));
updateLoading(100, 'Ready!');
await new Promise(r => setTimeout(r, 300));
assetsLoaded = true;
hideLoading();
initGame();
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
// Start music
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
try {
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
} catch(e) { console.log('Music not available'); }
}
}
// ═══════════════════════════════════════════════════════════════════════
// GAME LOGIC
// ═══════════════════════════════════════════════════════════════════════
function initGame() {
state.playerX = 100;
state.playerY = canvas.height - 200;
// Generate platforms
state.platforms = [];
const groundY = canvas.height - 50;
// Ground
state.platforms.push({ x: 0, y: groundY, w: canvas.width, h: 50, isGround: true });
// Floating platforms
for (let i = 0; i < GAME_CONFIG.gameplay.platformCount; i++) {
state.platforms.push({
x: 100 + i * (canvas.width / GAME_CONFIG.gameplay.platformCount),
y: groundY - 100 - Math.random() * 250,
w: 100 + Math.random() * 80,
h: 20
});
}
// Add coins on platforms
state.collectedCoins = [];
state.platforms.forEach((p, i) => {
if (!p.isGround && Math.random() > 0.3) {
state.collectedCoins.push({
x: p.x + p.w / 2,
y: p.y - 30,
collected: false,
bounce: Math.random() * Math.PI * 2
});
}
});
}
function updateGame(dt) {
const { gravity, jumpForce, moveSpeed } = GAME_CONFIG.gameplay;
// Horizontal movement
if (keys['ArrowLeft'] || keys['KeyA']) {
state.playerVelX = -moveSpeed;
state.facingRight = false;
if (state.isGrounded) state.currentAnim = 'walk';
} else if (keys['ArrowRight'] || keys['KeyD']) {
state.playerVelX = moveSpeed;
state.facingRight = true;
if (state.isGrounded) state.currentAnim = 'walk';
} else {
state.playerVelX *= 0.85;
if (state.isGrounded && Math.abs(state.playerVelX) < 10) {
state.currentAnim = 'idle';
}
}
// Jump
if ((keys['Space'] || keys['ArrowUp'] || keys['KeyW']) && state.isGrounded) {
state.playerVelY = -jumpForce;
state.isGrounded = false;
state.currentAnim = 'jump';
}
// Apply gravity
state.playerVelY += gravity * dt;
// Move player
state.playerX += state.playerVelX * dt;
state.playerY += state.playerVelY * dt;
// Platform collision
state.isGrounded = false;
const playerRect = {
x: state.playerX - 20,
y: state.playerY - 80,
w: 40,
h: 80
};
state.platforms.forEach(p => {
// Check if player is above platform and falling
if (state.playerVelY >= 0 &&
playerRect.x + playerRect.w > p.x &&
playerRect.x < p.x + p.w &&
playerRect.y + playerRect.h >= p.y &&
playerRect.y + playerRect.h <= p.y + p.h + state.playerVelY * dt + 5) {
state.playerY = p.y - 80;
state.playerVelY = 0;
state.isGrounded = true;
if (state.currentAnim === 'jump') state.currentAnim = 'idle';
}
});
// Animation when in air
if (!state.isGrounded) {
state.currentAnim = state.playerVelY < 0 ? 'jump' : 'jump';
}
// Screen bounds
if (state.playerX < 30) state.playerX = 30;
if (state.playerX > canvas.width - 30) state.playerX = canvas.width - 30;
// Fall death
if (state.playerY > canvas.height + 100) {
endGame();
}
// Coin collection
state.collectedCoins.forEach(coin => {
if (!coin.collected) {
coin.bounce += dt * 3;
const coinY = coin.y + Math.sin(coin.bounce) * 5;
const dx = state.playerX - coin.x;
const dy = (state.playerY - 40) - coinY;
if (Math.sqrt(dx*dx + dy*dy) < 35) {
coin.collected = true;
state.coins++;
state.score += 10;
updateScore(0);
// Particle effect
for (let i = 0; i < 8; i++) {
state.particles.push({
x: coin.x,
y: coinY,
vx: (Math.random() - 0.5) * 200,
vy: -Math.random() * 150 - 50,
life: 1,
color: '#FFD700'
});
}
}
}
});
// Update particles
state.particles = state.particles.filter(p => {
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 400 * dt;
p.life -= dt * 2;
return p.life > 0;
});
// Score over time
state.score += Math.floor(dt * 5);
updateScore(0);
state.animTime += dt * 1000;
}
function renderGame() {
// Background
if (window.BackgroundEngine) {
window.BackgroundEngine.render(
ctx,
GAME_CONFIG.assets.background.theme,
GAME_CONFIG.assets.background.variant,
canvas.width,
canvas.height,
state.animTime
);
} else {
ctx.fillStyle = '#228B22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Platforms
state.platforms.forEach(p => {
if (p.isGround) {
ctx.fillStyle = '#3d2817';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#4a7c59';
ctx.fillRect(p.x, p.y, p.w, 10);
} else {
// Floating platform
ctx.fillStyle = '#5d4037';
ctx.fillRect(p.x, p.y, p.w, p.h);
ctx.fillStyle = '#795548';
ctx.fillRect(p.x + 2, p.y + 2, p.w - 4, 6);
}
});
// Coins
state.collectedCoins.forEach(coin => {
if (!coin.collected) {
const coinY = coin.y + Math.sin(coin.bounce) * 5;
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(coin.x, coinY, 12, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFA500';
ctx.beginPath();
ctx.arc(coin.x - 3, coinY - 3, 4, 0, Math.PI * 2);
ctx.fill();
}
});
// Particles
state.particles.forEach(p => {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - 3, p.y - 3, 6, 6);
});
ctx.globalAlpha = 1;
// Player avatar
ctx.save();
ctx.translate(state.playerX, state.playerY);
if (!state.facingRight) {
ctx.scale(-1, 1);
}
ctx.translate(-32, -160);
if (window.AvatarSystem) {
window.AvatarSystem.render(
ctx,
playerAvatar,
GAME_CONFIG.assets.avatarContext.mode,
state.currentAnim,
state.animTime
);
} else {
// Fallback
ctx.fillStyle = '#4CAF50';
ctx.fillRect(12, 100, 40, 60);
ctx.fillStyle = '#FFCC80';
ctx.beginPath();
ctx.arc(32, 80, 25, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
// ═══════════════════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════════════════
function updateScore(points) {
document.getElementById('score').textContent = state.score;
document.getElementById('coins').textContent = state.coins;
}
function endGame() {
state.gameOver = true;
if (window.parent !== window) {
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
}
setTimeout(() => {
if (confirm(`Game Over!\nScore: ${state.score}\nCoins: ${state.coins}\n\nPlay again?`)) {
state.score = 0;
state.coins = 0;
state.gameOver = false;
initGame();
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
}, 100);
}
// ═══════════════════════════════════════════════════════════════════════
// INPUT
// ═══════════════════════════════════════════════════════════════════════
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'Escape') {
state.paused = !state.paused;
if (!state.paused) {
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
}
});
document.addEventListener('keyup', e => {
keys[e.code] = false;
});
// Touch controls
let touchStartX = 0;
canvas.addEventListener('touchstart', e => {
e.preventDefault();
touchStartX = e.touches[0].clientX;
const y = e.touches[0].clientY;
// Top half = jump
if (y < canvas.height / 2) {
keys['Space'] = true;
}
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
const x = e.touches[0].clientX;
const dx = x - touchStartX;
keys['ArrowLeft'] = dx < -30;
keys['ArrowRight'] = dx > 30;
}, { passive: false });
canvas.addEventListener('touchend', e => {
keys['ArrowLeft'] = false;
keys['ArrowRight'] = false;
keys['Space'] = false;
});
// ═══════════════════════════════════════════════════════════════════════
// GAME LOOP
// ═══════════════════════════════════════════════════════════════════════
function gameLoop(time) {
if (state.gameOver || state.paused) return;
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
state.lastTime = time;
updateGame(dt);
renderGame();
requestAnimationFrame(gameLoop);
}
// Parent window controls
window.pauseGame = () => { state.paused = true; };
window.resumeGame = () => {
state.paused = false;
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
};
// Start loading
loadAssets();
</script>
</body>
</html>
+702
View File
@@ -0,0 +1,702 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Night Racer - Racing Example</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; overflow: hidden; background: #0a0a15; touch-action: none; }
canvas { display: block; }
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: 'Courier New', monospace;
font-size: 16px;
text-shadow: 0 0 10px #0ff;
z-index: 10;
}
#ui div { margin-bottom: 5px; }
#speedometer {
position: absolute;
bottom: 20px;
right: 20px;
color: #0ff;
font-family: 'Courier New', monospace;
font-size: 48px;
font-weight: bold;
text-shadow: 0 0 20px #0ff;
}
#loading {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, #0a0a15, #1a1a2e);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #0ff;
font-family: 'Courier New', monospace;
z-index: 100;
}
#loading h1 {
margin-bottom: 1rem;
text-shadow: 0 0 20px #0ff;
font-size: 2.5rem;
}
.loading-bar {
width: 250px;
height: 4px;
background: #1a1a2e;
border: 1px solid #0ff;
}
.loading-bar-fill {
height: 100%;
background: #0ff;
width: 0%;
transition: width 0.3s;
box-shadow: 0 0 10px #0ff;
}
</style>
</head>
<body>
<div id="loading">
<h1>NIGHT RACER</h1>
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
<p id="loadingText">Initializing...</p>
</div>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div>SCORE: <span id="score">0</span></div>
<div>DISTANCE: <span id="distance">0</span>m</div>
</div>
<div id="speedometer"><span id="speed">0</span> km/h</div>
<!-- Asset Libraries -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/urban.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/music/musicEngine.js"></script>
<script src="/assets/music/library/songs1-80.js"></script>
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<script src="/assets/avatar/contexts/vehicles.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<script>
// ═══════════════════════════════════════════════════════════════════════
// RACER EXAMPLE - Vehicle Context Demo
// ═══════════════════════════════════════════════════════════════════════
// Demonstrates: HEAD_AND_SHOULDERS avatar, race-car context,
// urban/neonDistrict background, Intense music mood
const GAME_CONFIG = {
title: "Night Racer",
assets: {
music: { mood: "Intense", energy: 9, enabled: true },
background: { theme: "urban", variant: "neonDistrict", animated: true },
avatarContext: { mode: "HEAD_AND_SHOULDERS", context: "race-car", showAccessories: true }
},
gameplay: {
baseSpeed: 300,
maxSpeed: 600,
acceleration: 150,
deceleration: 200,
laneWidth: 100,
lanes: 3,
obstacleSpawnRate: 1200,
coinSpawnRate: 800
}
};
const playerAvatar = {
faceShape: 'square',
skinTone: 'tan',
hairStyle: 'short',
hairColor: '#1a1a1a',
eyeShape: 'sharp',
eyeColor: '#2ecc71',
noseShape: 'pointed',
mouthShape: 'serious'
};
// ═══════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
let state = {
playerLane: 1,
playerX: 0,
targetX: 0,
speed: 0,
distance: 0,
animTime: 0,
score: 0,
gameOver: false,
paused: false,
lastTime: 0,
obstacles: [],
coins: [],
roadLines: [],
buildings: [],
lastObstacle: 0,
lastCoin: 0
};
const keys = {};
// ═══════════════════════════════════════════════════════════════════════
// LOADING
// ═══════════════════════════════════════════════════════════════════════
function updateLoading(progress, text) {
document.getElementById('loadingFill').style.width = progress + '%';
document.getElementById('loadingText').textContent = text;
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
}
async function loadAssets() {
updateLoading(20, 'Loading city...');
await new Promise(r => setTimeout(r, 200));
updateLoading(40, 'Loading music...');
await new Promise(r => setTimeout(r, 200));
updateLoading(60, 'Loading vehicle...');
await new Promise(r => setTimeout(r, 200));
updateLoading(80, 'Starting engine...');
await new Promise(r => setTimeout(r, 200));
updateLoading(100, 'GO!');
await new Promise(r => setTimeout(r, 300));
hideLoading();
initGame();
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
try {
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
} catch(e) {}
}
}
// ═══════════════════════════════════════════════════════════════════════
// GAME LOGIC
// ═══════════════════════════════════════════════════════════════════════
function initGame() {
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
const roadWidth = laneWidth * lanes;
const roadX = canvas.width / 2 - roadWidth / 2;
state.playerLane = 1;
state.playerX = roadX + laneWidth * state.playerLane + laneWidth / 2;
state.targetX = state.playerX;
state.speed = GAME_CONFIG.gameplay.baseSpeed;
state.distance = 0;
state.score = 0;
state.obstacles = [];
state.coins = [];
state.lastObstacle = 0;
state.lastCoin = 0;
// Initialize road lines
state.roadLines = [];
for (let i = 0; i < 15; i++) {
state.roadLines.push({
y: i * 80,
lane: 0
});
state.roadLines.push({
y: i * 80,
lane: 1
});
}
// Initialize buildings
state.buildings = [];
for (let i = 0; i < 20; i++) {
state.buildings.push({
x: Math.random() > 0.5 ? -50 - Math.random() * 100 : canvas.width + 50 + Math.random() * 100,
y: i * 100 - 500,
width: 60 + Math.random() * 100,
height: 150 + Math.random() * 300,
color: `hsl(${260 + Math.random() * 40}, 50%, ${15 + Math.random() * 15}%)`
});
}
updateUI();
}
function getLaneX(lane) {
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
const roadWidth = laneWidth * lanes;
const roadX = canvas.width / 2 - roadWidth / 2;
return roadX + laneWidth * lane + laneWidth / 2;
}
function updateGame(dt) {
const { baseSpeed, maxSpeed, acceleration, deceleration, laneWidth, lanes, obstacleSpawnRate, coinSpawnRate } = GAME_CONFIG.gameplay;
// Speed control
if (keys['ArrowUp'] || keys['KeyW']) {
state.speed = Math.min(state.speed + acceleration * dt, maxSpeed);
} else {
state.speed = Math.max(state.speed - deceleration * dt * 0.3, baseSpeed);
}
// Lane switching
if (keys['ArrowLeft'] || keys['KeyA']) {
if (!keys._leftPressed) {
keys._leftPressed = true;
state.playerLane = Math.max(0, state.playerLane - 1);
state.targetX = getLaneX(state.playerLane);
}
} else {
keys._leftPressed = false;
}
if (keys['ArrowRight'] || keys['KeyD']) {
if (!keys._rightPressed) {
keys._rightPressed = true;
state.playerLane = Math.min(lanes - 1, state.playerLane + 1);
state.targetX = getLaneX(state.playerLane);
}
} else {
keys._rightPressed = false;
}
// Smooth lane movement
state.playerX += (state.targetX - state.playerX) * 10 * dt;
// Update distance
state.distance += state.speed * dt * 0.01;
// Spawn obstacles
state.lastObstacle += dt * 1000;
if (state.lastObstacle > obstacleSpawnRate * (baseSpeed / state.speed)) {
state.lastObstacle = 0;
const lane = Math.floor(Math.random() * lanes);
state.obstacles.push({
x: getLaneX(lane),
y: -100,
lane: lane,
type: Math.random() > 0.7 ? 'truck' : 'car',
color: `hsl(${Math.random() * 360}, 70%, 50%)`
});
}
// Spawn coins
state.lastCoin += dt * 1000;
if (state.lastCoin > coinSpawnRate) {
state.lastCoin = 0;
const lane = Math.floor(Math.random() * lanes);
// Don't spawn on obstacles
const blocked = state.obstacles.some(o => o.lane === lane && o.y < 200);
if (!blocked) {
state.coins.push({
x: getLaneX(lane),
y: -50,
lane: lane,
rotation: 0
});
}
}
// Update obstacles
state.obstacles = state.obstacles.filter(obs => {
obs.y += state.speed * dt;
// Collision check
const playerY = canvas.height - 150;
if (Math.abs(obs.x - state.playerX) < 40 &&
Math.abs(obs.y - playerY) < 60) {
endGame();
}
return obs.y < canvas.height + 150;
});
// Update coins
state.coins = state.coins.filter(coin => {
coin.y += state.speed * dt;
coin.rotation += dt * 5;
// Collection check
const playerY = canvas.height - 150;
if (Math.abs(coin.x - state.playerX) < 50 &&
Math.abs(coin.y - playerY) < 50) {
state.score += 10;
return false;
}
return coin.y < canvas.height + 50;
});
// Update road lines
state.roadLines.forEach(line => {
line.y += state.speed * dt;
if (line.y > canvas.height) {
line.y -= 15 * 80;
}
});
// Update buildings
state.buildings.forEach(b => {
b.y += state.speed * dt * 0.3;
if (b.y > canvas.height + 200) {
b.y -= 20 * 100 + 500;
b.height = 150 + Math.random() * 300;
}
});
// Score over time
state.score += Math.floor(state.speed * dt * 0.05);
state.animTime += dt * 1000;
updateUI();
}
function renderGame() {
// Dark background
ctx.fillStyle = '#0a0a15';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// City skyline (buildings)
state.buildings.forEach(b => {
// Building
ctx.fillStyle = b.color;
const bx = b.x < 0 ? b.x : b.x;
ctx.fillRect(bx, b.y, b.width, b.height);
// Windows
ctx.fillStyle = Math.random() > 0.3 ? '#ffff88' : '#333';
for (let wy = b.y + 20; wy < b.y + b.height - 20; wy += 30) {
for (let wx = bx + 10; wx < bx + b.width - 10; wx += 20) {
if (Math.random() > 0.4) {
ctx.fillRect(wx, wy, 10, 15);
}
}
}
});
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
const roadWidth = laneWidth * lanes + 40;
const roadX = canvas.width / 2 - roadWidth / 2;
// Road
ctx.fillStyle = '#1a1a2a';
ctx.fillRect(roadX, 0, roadWidth, canvas.height);
// Road edges (neon)
ctx.strokeStyle = '#ff00ff';
ctx.lineWidth = 3;
ctx.shadowColor = '#ff00ff';
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.moveTo(roadX, 0);
ctx.lineTo(roadX, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(roadX + roadWidth, 0);
ctx.lineTo(roadX + roadWidth, canvas.height);
ctx.stroke();
ctx.shadowBlur = 0;
// Lane dividers
ctx.strokeStyle = '#0ff';
ctx.lineWidth = 2;
ctx.shadowColor = '#0ff';
ctx.shadowBlur = 10;
state.roadLines.forEach(line => {
const x1 = roadX + 20 + laneWidth * (line.lane + 1);
ctx.beginPath();
ctx.moveTo(x1, line.y);
ctx.lineTo(x1, line.y + 40);
ctx.stroke();
});
ctx.shadowBlur = 0;
// Coins
state.coins.forEach(coin => {
ctx.save();
ctx.translate(coin.x, coin.y);
ctx.rotate(coin.rotation);
ctx.fillStyle = '#FFD700';
ctx.shadowColor = '#FFD700';
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.arc(0, 0, 15, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFA500';
ctx.beginPath();
ctx.arc(-4, -4, 5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.restore();
});
// Obstacles (other cars)
state.obstacles.forEach(obs => {
ctx.save();
ctx.translate(obs.x, obs.y);
if (obs.type === 'truck') {
// Truck
ctx.fillStyle = obs.color;
ctx.fillRect(-25, -60, 50, 100);
ctx.fillStyle = '#333';
ctx.fillRect(-20, -55, 40, 25);
// Wheels
ctx.fillStyle = '#111';
ctx.fillRect(-28, -40, 8, 20);
ctx.fillRect(20, -40, 8, 20);
ctx.fillRect(-28, 20, 8, 20);
ctx.fillRect(20, 20, 8, 20);
} else {
// Car
ctx.fillStyle = obs.color;
ctx.fillRect(-20, -40, 40, 70);
ctx.fillStyle = '#333';
ctx.fillRect(-15, -35, 30, 20);
// Tail lights
ctx.fillStyle = '#ff0000';
ctx.shadowColor = '#ff0000';
ctx.shadowBlur = 10;
ctx.fillRect(-18, 25, 8, 5);
ctx.fillRect(10, 25, 8, 5);
ctx.shadowBlur = 0;
}
ctx.restore();
});
// Player car with avatar
const playerY = canvas.height - 150;
ctx.save();
ctx.translate(state.playerX, playerY);
// Car body
ctx.fillStyle = '#e74c3c';
ctx.shadowColor = '#e74c3c';
ctx.shadowBlur = 20;
// Main body
ctx.beginPath();
ctx.moveTo(-30, 50);
ctx.lineTo(-35, 20);
ctx.lineTo(-30, -30);
ctx.lineTo(-20, -50);
ctx.lineTo(20, -50);
ctx.lineTo(30, -30);
ctx.lineTo(35, 20);
ctx.lineTo(30, 50);
ctx.closePath();
ctx.fill();
ctx.shadowBlur = 0;
// Windshield
ctx.fillStyle = '#1a1a2e';
ctx.beginPath();
ctx.moveTo(-20, -45);
ctx.lineTo(-25, -15);
ctx.lineTo(25, -15);
ctx.lineTo(20, -45);
ctx.closePath();
ctx.fill();
// Avatar in car (head and shoulders visible)
if (window.ContextRenderer) {
ctx.save();
ctx.translate(-32, -65);
window.ContextRenderer.render(
ctx,
GAME_CONFIG.assets.avatarContext.context,
playerAvatar,
32, 32,
'idle',
state.animTime
);
ctx.restore();
} else if (window.AvatarSystem) {
ctx.save();
ctx.translate(-32, -65);
window.AvatarSystem.render(
ctx,
playerAvatar,
'HEAD_AND_SHOULDERS',
'idle',
state.animTime
);
ctx.restore();
}
// Headlights
ctx.fillStyle = '#fff';
ctx.shadowColor = '#fff';
ctx.shadowBlur = 30;
ctx.beginPath();
ctx.arc(-18, -45, 5, 0, Math.PI * 2);
ctx.arc(18, -45, 5, 0, Math.PI * 2);
ctx.fill();
// Light beams
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
ctx.beginPath();
ctx.moveTo(-20, -50);
ctx.lineTo(-40, -200);
ctx.lineTo(0, -200);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(20, -50);
ctx.lineTo(0, -200);
ctx.lineTo(40, -200);
ctx.closePath();
ctx.fill();
ctx.shadowBlur = 0;
ctx.restore();
// Speed lines effect at high speed
if (state.speed > 400) {
ctx.strokeStyle = `rgba(0, 255, 255, ${(state.speed - 400) / 400})`;
ctx.lineWidth = 2;
for (let i = 0; i < 10; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + 50 + state.speed * 0.2);
ctx.stroke();
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════════════════
function updateUI() {
document.getElementById('score').textContent = state.score;
document.getElementById('distance').textContent = Math.floor(state.distance);
document.getElementById('speed').textContent = Math.floor(state.speed * 0.6);
}
function endGame() {
state.gameOver = true;
if (window.parent !== window) {
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
}
setTimeout(() => {
if (confirm(`GAME OVER\n\nScore: ${state.score}\nDistance: ${Math.floor(state.distance)}m\n\nRace again?`)) {
state.gameOver = false;
initGame();
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
}, 100);
}
// ═══════════════════════════════════════════════════════════════════════
// INPUT
// ═══════════════════════════════════════════════════════════════════════
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'Escape') {
state.paused = !state.paused;
if (!state.paused) {
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
}
});
document.addEventListener('keyup', e => {
keys[e.code] = false;
});
// Touch controls
let touchStartX = 0;
canvas.addEventListener('touchstart', e => {
e.preventDefault();
touchStartX = e.touches[0].clientX;
keys['ArrowUp'] = true;
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
const x = e.touches[0].clientX;
const dx = x - touchStartX;
if (dx < -50) {
keys['ArrowLeft'] = true;
keys['ArrowRight'] = false;
touchStartX = x;
} else if (dx > 50) {
keys['ArrowRight'] = true;
keys['ArrowLeft'] = false;
touchStartX = x;
}
}, { passive: false });
canvas.addEventListener('touchend', e => {
keys['ArrowUp'] = false;
keys['ArrowLeft'] = false;
keys['ArrowRight'] = false;
});
// ═══════════════════════════════════════════════════════════════════════
// GAME LOOP
// ═══════════════════════════════════════════════════════════════════════
function gameLoop(time) {
if (state.gameOver || state.paused) return;
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
state.lastTime = time;
updateGame(dt);
renderGame();
requestAnimationFrame(gameLoop);
}
window.pauseGame = () => { state.paused = true; };
window.resumeGame = () => {
state.paused = false;
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
};
loadAssets();
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
This is a placeholder - we'll create a proper icon
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#8b5cf6"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="96" fill="url(#bg)"/>
<text x="256" y="340" font-family="Arial, sans-serif" font-size="280" text-anchor="middle" fill="white">🎮</text>
</svg>

After

Width:  |  Height:  |  Size: 461 B

+56
View File
@@ -0,0 +1,56 @@
// Push notification handler for GamerComp PWA
// This file is loaded by the service worker via importScripts
self.addEventListener('push', function(event) {
if (!event.data) return;
try {
const data = event.data.json();
const options = {
body: data.body || 'You have a new notification',
icon: '/icon.svg',
badge: '/icon.svg',
vibrate: [100, 50, 100],
data: {
url: data.url || '/'
},
actions: [
{ action: 'open', title: 'Open' }
]
};
event.waitUntil(
self.registration.showNotification(data.title || 'GamerComp', options)
);
} catch (err) {
// Fallback for non-JSON payloads
event.waitUntil(
self.registration.showNotification('GamerComp', {
body: event.data.text(),
icon: '/icon.svg'
})
);
}
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
const url = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
// Try to focus an existing window
for (const client of clientList) {
if (client.url.includes('gamercomp.com') && 'focus' in client) {
client.navigate(url);
return client.focus();
}
}
// Open new window if none exists
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
+451
View File
@@ -0,0 +1,451 @@
# Game Template Guide for Haiku
This guide explains how to use the game templates to generate HTML5 canvas games for GamerComp.
## Template Selection
### `gameTemplateAssets.html` - Full Asset Library Template
Use this template when the game needs:
- Procedural background themes (120 options)
- Procedural music (160 songs, 8 moods)
- User's avatar with game contexts (26 contexts)
- Accessories and effects
**Best for:** RPGs, adventure games, racing games, space shooters, platformers, any game where the player's avatar matters.
### `gameTemplateSimple.html` - Minimal Template
Use this template when the game needs:
- Simple graphics (shapes, basic sprites)
- No avatar/user identity
- Fast loading (no asset libraries)
**Best for:** Puzzle games, simple arcade games, physics toys, creative tools, games where the player controls an object (not an avatar).
---
## Using gameTemplateAssets.html
### Step 1: Fill in GAME_CONFIG
Replace the placeholder comments with actual values:
```javascript
const GAME_CONFIG = {
title: "Space Defender", // Replace <!-- GAME_TITLE -->
assets: {
music: {
mood: "Epic", // Replace <!-- MUSIC_MOOD -->
energy: 8,
enabled: true
},
background: {
theme: "space", // Replace <!-- BG_THEME -->
variant: "nebula", // Replace <!-- BG_VARIANT -->
animated: true,
effects: ["stars"]
},
avatarContext: {
mode: "HEAD_ONLY", // Replace <!-- AVATAR_MODE -->
context: "spaceship-cockpit", // Replace <!-- CONTEXT_TYPE -->
showAccessories: true
}
},
gameplay: {
// Add your game-specific config
enemySpeed: 200,
spawnRate: 2000,
bulletSpeed: 500
}
};
```
### Step 2: Choose Asset Combinations
#### Music Moods (8 options)
| Mood | Best For | Energy Range |
|------|----------|--------------|
| Epic | Boss battles, action games | 7-10 |
| Chill | Puzzle games, casual games | 2-4 |
| Intense | Racing, shooters, action | 8-10 |
| Playful | Kids games, casual games | 5-7 |
| Mysterious | Horror, mystery, exploration | 3-6 |
| Heroic | Adventure, platformers | 6-8 |
| Quirky | Puzzle, comedy, unique games | 4-7 |
| Ambient | Background, meditation, creative | 1-3 |
#### Background Themes (15 themes × 8 variants each)
| Theme | Variants | Best For |
|-------|----------|----------|
| space | nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole | Space games, sci-fi |
| nature | forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow | Adventure, relaxing |
| urban | citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley | Racing, action |
| fantasy | castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower | RPG, adventure |
| abstract | geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix | Puzzle, creative |
| retro | pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave | Arcade, retro |
| indoor | laboratory, classroom, bedroom, kitchen, arcade, office, library, gym | Simulation, casual |
| weather | rain, snow, storm, sunset, sunrise, fog, lightning, aurora | Atmospheric |
| sports | stadium, raceTrack, court, field, arena, pool, gym, skatepark | Sports games |
| seasonal | springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical | Seasonal themes |
| underwater | coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure | Ocean games |
| sky | clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield | Flying games |
| mechanical | gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab | Tech, puzzle |
| spooky | hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows | Horror, Halloween |
| colorful | rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism | Kids, casual |
#### Avatar Modes (4 options)
| Mode | Dimensions | Use When |
|------|------------|----------|
| HEAD_ONLY | 64×64 | Cockpit view, flying games, maze games |
| HEAD_AND_SHOULDERS | 64×96 | Driving games, submarine, conversation |
| UPPER_BODY | 64×128 | Racing, shooting, hoverboard |
| FULL_BODY | 64×160 | Platformers, RPGs, sports, adventure |
#### Avatar Contexts (26 options)
**Vehicle Contexts:**
- `spaceship-cockpit` (HEAD_ONLY) - Space shooters
- `race-car` (HEAD_AND_SHOULDERS) - Racing games
- `airplane` (HEAD_AND_SHOULDERS) - Flying games
- `flappy-style` (HEAD_ONLY) - Flappy bird clones
- `tank` (HEAD_ONLY) - Tank games
- `pacman-style` (HEAD_ONLY) - Maze games
- `hoverboard` (UPPER_BODY) - Hoverboard games
- `jetpack` (UPPER_BODY) - Jetpack games
- `mech-suit` (HEAD_ONLY) - Mech combat
- `submarine` (HEAD_AND_SHOULDERS) - Underwater
- `dragon-rider` (FULL_BODY) - Dragon games
- `motorcycle` (FULL_BODY) - Motorcycle racing
**Costume Contexts:**
- `knight-armor` (FULL_BODY) - Medieval games
- `space-suit` (FULL_BODY) - Space exploration
- `ninja-outfit` (FULL_BODY) - Ninja games
- `wizard-robes` (FULL_BODY) - Magic games
- `superhero-cape` (FULL_BODY) - Hero games
- `athlete-uniform` (FULL_BODY) - Sports
- `pirate-outfit` (FULL_BODY) - Pirate games
- `scientist-labcoat` (FULL_BODY) - Science games
- `chef-outfit` (FULL_BODY) - Cooking games
- `cowboy-gear` (FULL_BODY) - Western games
**Pure Contexts:**
- `platformer-standard` (FULL_BODY) - Standard platformer
- `sports-player` (FULL_BODY) - Sports games
- `adventure-hero` (FULL_BODY) - Adventure games
- `runner` (FULL_BODY) - Endless runners
### Step 3: Implement Game Functions
The template provides these functions for you to implement:
```javascript
// Called once at start and on restart
function initGame() {
// Initialize enemies, items, level data
state.enemies = [];
state.items = [];
}
// Called every frame (dt = seconds since last frame)
function updateGame(dt) {
// Update positions, check collisions, spawn objects
state.enemies.forEach(e => {
e.x += e.velX * dt;
e.y += e.velY * dt;
});
// Check player collision with enemies
state.enemies.forEach(e => {
if (circleCollision(
{ x: state.playerX, y: state.playerY, r: 30 },
{ x: e.x, y: e.y, r: e.radius }
)) {
updateHealth(-10);
}
});
}
// Called every frame after updateGame
function renderGame() {
// Draw enemies, items, effects
state.enemies.forEach(e => {
ctx.fillStyle = e.color;
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
ctx.fill();
});
}
// Input handlers
function handleKeyDown(code) {
if (code === 'Space') {
shoot(); // Your function
}
}
function handleTouchStart(x, y) {
// Handle tap
}
```
### Step 4: Use Provided Utilities
The template includes these utility functions:
```javascript
// Score & Health
updateScore(10); // Add 10 points
updateHealth(-5); // Lose 5 health
updateHealth(10); // Gain 10 health (capped at 100)
// Animation
setPlayerAnimation('walk'); // idle, walk, run, jump, hurt, celebrate, etc.
// Math
randomInt(1, 10); // Random integer 1-10
randomFloat(0, 1); // Random float 0-1
clamp(value, 0, 100); // Clamp to range
lerp(0, 100, 0.5); // Linear interpolation = 50
// Collision
rectCollision(r1, r2); // {x, y, width, height} objects
circleCollision(c1, c2); // {x, y, radius} objects
// Game flow
endGame(); // Trigger game over
```
---
## Using gameTemplateSimple.html
For simple games without avatars:
```javascript
const GAME_CONFIG = {
title: "Catch the Stars",
background: {
type: "gradient",
colors: ["#0f0c29", "#302b63", "#24243e"]
},
player: {
width: 50,
height: 50,
color: "#FFD700",
speed: 400
},
gameplay: {
starSpawnRate: 1000,
starFallSpeed: 200
}
};
```
Then implement:
- `initGame()` - Setup
- `update(dt)` - Game logic
- `render()` - Draw objects
- Input handlers
---
## Common Patterns
### Platformer Movement
```javascript
const GRAVITY = 1500;
const JUMP_FORCE = 600;
function update(dt) {
// Horizontal movement
if (keys['ArrowLeft']) state.playerVelX = -300;
else if (keys['ArrowRight']) state.playerVelX = 300;
else state.playerVelX *= 0.8; // Friction
// Apply gravity
state.playerVelY += GRAVITY * dt;
// Move player
state.playerX += state.playerVelX * dt;
state.playerY += state.playerVelY * dt;
// Ground collision
if (state.playerY > canvas.height - 50) {
state.playerY = canvas.height - 50;
state.playerVelY = 0;
state.isGrounded = true;
}
}
function handleKeyDown(code) {
if (code === 'Space' && state.isGrounded) {
state.playerVelY = -JUMP_FORCE;
state.isGrounded = false;
setPlayerAnimation('jump');
}
}
```
### Space Shooter
```javascript
state.bullets = [];
state.enemies = [];
function update(dt) {
// Update bullets
state.bullets = state.bullets.filter(b => {
b.y -= 500 * dt;
return b.y > 0;
});
// Spawn enemies
if (Math.random() < 0.02) {
state.enemies.push({
x: randomInt(50, canvas.width - 50),
y: -30,
radius: 20,
color: '#ff4444'
});
}
// Update enemies
state.enemies.forEach(e => e.y += 150 * dt);
state.enemies = state.enemies.filter(e => e.y < canvas.height + 50);
// Collision: bullets vs enemies
state.bullets.forEach(b => {
state.enemies = state.enemies.filter(e => {
if (circleCollision(b, e)) {
updateScore(10);
return false;
}
return true;
});
});
}
function handleKeyDown(code) {
if (code === 'Space') {
state.bullets.push({
x: state.playerX,
y: state.playerY - 30,
radius: 5
});
}
}
```
### Endless Runner
```javascript
let scrollSpeed = 300;
let obstacles = [];
let lastObstacle = 0;
function update(dt) {
scrollSpeed += dt * 2; // Speed up over time
// Jump
if (keys['Space'] && state.isGrounded) {
state.playerVelY = -500;
state.isGrounded = false;
}
// Gravity
state.playerVelY += 1200 * dt;
state.playerY += state.playerVelY * dt;
// Ground
const ground = canvas.height - 100;
if (state.playerY > ground) {
state.playerY = ground;
state.isGrounded = true;
}
// Spawn obstacles
lastObstacle += dt * 1000;
if (lastObstacle > 1500) {
obstacles.push({ x: canvas.width, y: ground, w: 30, h: 50 });
lastObstacle = 0;
}
// Move obstacles
obstacles = obstacles.filter(o => {
o.x -= scrollSpeed * dt;
return o.x > -50;
});
// Score
updateScore(Math.floor(dt * 10));
}
```
---
## Recommended Combinations by Game Type
### Space Shooter
```javascript
music: { mood: "Epic", energy: 8 }
background: { theme: "space", variant: "nebula" }
avatarContext: { mode: "HEAD_ONLY", context: "spaceship-cockpit" }
```
### Platformer
```javascript
music: { mood: "Playful", energy: 6 }
background: { theme: "nature", variant: "forest" }
avatarContext: { mode: "FULL_BODY", context: "platformer-standard" }
```
### Racing
```javascript
music: { mood: "Intense", energy: 9 }
background: { theme: "urban", variant: "street" }
avatarContext: { mode: "HEAD_AND_SHOULDERS", context: "race-car" }
```
### RPG/Adventure
```javascript
music: { mood: "Heroic", energy: 6 }
background: { theme: "fantasy", variant: "castle" }
avatarContext: { mode: "FULL_BODY", context: "knight-armor" }
```
### Puzzle
```javascript
music: { mood: "Chill", energy: 3 }
background: { theme: "abstract", variant: "minimalist" }
// Use gameTemplateSimple.html instead
```
### Horror
```javascript
music: { mood: "Mysterious", energy: 4 }
background: { theme: "spooky", variant: "hauntedHouse" }
avatarContext: { mode: "FULL_BODY", context: "adventure-hero" }
```
---
## Important Notes
1. **Always use the provided state object** - Don't create global variables for game state.
2. **Delta time is in seconds** - Multiply speeds by `dt` for frame-independent movement.
3. **Player position is center-based** - `playerX` and `playerY` are the center of the player.
4. **Earned accessories always show** - User's crowns, auras, etc. render above costumes.
5. **Test on mobile** - Touch handlers are provided, but test gesture-based games.
6. **Use endGame() properly** - It handles score reporting and music fadeout.
7. **Don't modify the asset library scripts** - Only modify the game logic section.
@@ -0,0 +1,905 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><!-- GAME_TITLE --></title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
overflow: hidden;
background: #000;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
canvas { display: block; }
/* Game UI Overlay */
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 16px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
z-index: 100;
pointer-events: none;
}
/* Pause Menu */
#pauseMenu {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.9);
padding: 30px;
border-radius: 15px;
text-align: center;
color: white;
font-family: Arial, sans-serif;
z-index: 200;
}
#pauseMenu h2 { margin-top: 0; }
#pauseMenu button {
display: block;
width: 200px;
margin: 10px auto;
padding: 12px;
font-size: 16px;
border: none;
border-radius: 8px;
cursor: pointer;
background: #4CAF50;
color: white;
}
#pauseMenu button:hover { background: #45a049; }
/* Loading Screen */
#loading {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-family: Arial, sans-serif;
z-index: 300;
}
#loading .spinner {
width: 50px;
height: 50px;
border: 4px solid #333;
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#loading .progress {
margin-top: 20px;
font-size: 14px;
color: #888;
}
</style>
</head>
<body>
<!-- Loading Screen -->
<div id="loading">
<div class="spinner"></div>
<div class="progress">Loading assets...</div>
</div>
<!-- Game Canvas -->
<canvas id="gameCanvas"></canvas>
<!-- HUD Overlay -->
<div id="ui">
<div id="score">Score: 0</div>
<div id="health">Health: 100</div>
<!-- HAIKU: Add additional UI elements here -->
</div>
<!-- Pause Menu -->
<div id="pauseMenu">
<h2>PAUSED</h2>
<button onclick="resumeGame()">Resume</button>
<button onclick="toggleMusic()">Music: ON</button>
<button onclick="toggleSfx()">Sound: ON</button>
<button onclick="restartGame()">Restart</button>
</div>
<!-- ═══════════════════════════════════════════════════════════════════
ASSET LIBRARY SCRIPTS
These provide: Music, Backgrounds, Avatars, Contexts
═══════════════════════════════════════════════════════════════════ -->
<!-- Music System (160 procedural songs) -->
<script src="/assets/music/moodCategories.js"></script>
<script src="/assets/music/musicLibrary.js"></script>
<script src="/assets/music/musicEngine.js"></script>
<!-- Background System (120 procedural backgrounds) -->
<script src="/assets/backgrounds/backgroundEngine.js"></script>
<script src="/assets/backgrounds/effects.js"></script>
<script src="/assets/backgrounds/themes/space.js"></script>
<script src="/assets/backgrounds/themes/nature.js"></script>
<script src="/assets/backgrounds/themes/urban.js"></script>
<script src="/assets/backgrounds/themes/fantasy.js"></script>
<script src="/assets/backgrounds/themes/abstract.js"></script>
<script src="/assets/backgrounds/themes/retro.js"></script>
<script src="/assets/backgrounds/themes/indoor.js"></script>
<script src="/assets/backgrounds/themes/weather.js"></script>
<script src="/assets/backgrounds/themes/sports.js"></script>
<script src="/assets/backgrounds/themes/seasonal.js"></script>
<script src="/assets/backgrounds/themes/underwater.js"></script>
<script src="/assets/backgrounds/themes/sky.js"></script>
<script src="/assets/backgrounds/themes/mechanical.js"></script>
<script src="/assets/backgrounds/themes/spooky.js"></script>
<script src="/assets/backgrounds/themes/colorful.js"></script>
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
<!-- Avatar System -->
<script src="/assets/avatar/avatarData.js"></script>
<script src="/assets/avatar/accessories.js"></script>
<script src="/assets/avatar/animations.js"></script>
<script src="/assets/avatar/avatarRenderer.js"></script>
<script src="/assets/avatar/accessoryRenderer.js"></script>
<script src="/assets/avatar/avatarSystem.js"></script>
<!-- Context System (26 game contexts) -->
<script src="/assets/avatar/contexts/vehicles.js"></script>
<script src="/assets/avatar/contexts/costumes.js"></script>
<script src="/assets/avatar/contexts/pure.js"></script>
<script src="/assets/avatar/contextCatalog.js"></script>
<script src="/assets/avatar/contextRenderer.js"></script>
<!-- Asset Manager (Unified coordination) -->
<script src="/assets/assetCatalog.js"></script>
<script src="/assets/compatibility.js"></script>
<script src="/assets/presets.js"></script>
<script src="/assets/assetManager.js"></script>
<script>
// ═══════════════════════════════════════════════════════════════════════
// GAME CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════
// HAIKU: Fill in the values below based on the game's theme and mechanics
const GAME_CONFIG = {
// Game metadata
title: "<!-- GAME_TITLE -->",
version: "1.0.0",
// Asset configuration
assets: {
// Music settings
// Moods: Epic, Chill, Intense, Playful, Mysterious, Heroic, Quirky, Ambient
music: {
mood: "<!-- MUSIC_MOOD -->", // e.g., "Epic", "Playful", "Mysterious"
energy: 7, // 1-10 (affects tempo/intensity)
enabled: true,
fadeInDuration: 1000 // ms to fade in music
},
// Background settings
// Themes: space, nature, urban, fantasy, abstract, retro, indoor, weather,
// sports, seasonal, underwater, sky, mechanical, spooky, colorful
background: {
theme: "<!-- BG_THEME -->", // e.g., "space", "nature", "fantasy"
variant: "<!-- BG_VARIANT -->", // e.g., "nebula", "forest", "castle"
animated: true, // Enable background animation
parallax: false, // Enable parallax scrolling
effects: [] // Additional effects: ["stars", "rain", etc]
},
// Avatar context settings
// Modes: HEAD_ONLY, HEAD_AND_SHOULDERS, UPPER_BODY, FULL_BODY
// Contexts: spaceship-cockpit, race-car, platformer-standard, knight-armor, etc.
avatarContext: {
mode: "<!-- AVATAR_MODE -->", // Display mode
context: "<!-- CONTEXT_TYPE -->", // Context ID
showAccessories: true, // Show earned accessories
scale: 1.0 // Avatar scale multiplier
},
// Color palette (optional override)
// Palettes: space-blue, forest-green, sunset-orange, ocean-teal, neon-pink,
// retro-arcade, fantasy-purple, spooky-grey, sports-red, nature-brown
colorPalette: null // null = auto-detect from theme
},
// Gameplay settings
// HAIKU: Define game-specific variables here
gameplay: {
// <!-- GAMEPLAY_CONFIG -->
// Example fields (customize per game):
// playerSpeed: 5,
// gravity: 0.5,
// jumpForce: 12,
// spawnRate: 2000,
// difficulty: 1
},
// Control scheme
controls: {
type: "<!-- CONTROL_TYPE -->", // "keyboard", "touch", "mouse", "tilt"
// HAIKU: Define specific bindings in input handlers below
}
};
// ═══════════════════════════════════════════════════════════════════════
// GAME STATE
// ═══════════════════════════════════════════════════════════════════════
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Core state
let gameState = {
// Player
player: null, // Avatar with context
playerX: 0,
playerY: 0,
playerVelX: 0,
playerVelY: 0,
// Game progress
score: 0,
health: 100,
level: 1,
// Game status
isRunning: false,
isPaused: false,
isGameOver: false,
// Timing
lastTime: 0,
deltaTime: 0,
elapsedTime: 0,
// Animation
currentAnimation: 'idle',
animationTime: 0,
// HAIKU: Add game-specific state variables below
// <!-- GAME_STATE -->
};
// Settings (persisted)
let settings = {
musicEnabled: true,
sfxEnabled: true,
musicVolume: 0.7,
sfxVolume: 0.8
};
// ═══════════════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════════════
// Resize canvas to fill screen
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Update loading progress
function setLoadingProgress(message) {
const progress = document.querySelector('#loading .progress');
if (progress) progress.textContent = message;
}
// Main initialization
async function init() {
try {
setLoadingProgress('Initializing asset systems...');
// Initialize asset manager
await AssetManager.initialize();
setLoadingProgress('Loading music...');
// Setup music
if (GAME_CONFIG.assets.music.enabled && window.MusicEngine) {
MusicEngine.setVolume(settings.musicVolume);
}
setLoadingProgress('Loading background...');
// Background is rendered each frame, no preload needed
setLoadingProgress('Loading avatar...');
// Load user's avatar
const userAvatar = await loadUserAvatar();
// Apply game context to avatar
if (window.ContextRenderer) {
gameState.player = ContextRenderer.apply(
userAvatar,
GAME_CONFIG.assets.avatarContext.context
);
} else {
// Fallback: use avatar directly
gameState.player = { avatar: userAvatar, context: null };
}
// Position player
gameState.playerX = canvas.width / 2;
gameState.playerY = canvas.height / 2;
setLoadingProgress('Starting game...');
// Initialize game-specific logic
initGame();
// Hide loading screen
document.getElementById('loading').style.display = 'none';
// Start music
if (GAME_CONFIG.assets.music.enabled && window.MusicEngine) {
MusicEngine.playRandomSong(GAME_CONFIG.assets.music.mood);
}
// Start game loop
gameState.isRunning = true;
gameState.lastTime = performance.now();
requestAnimationFrame(gameLoop);
} catch (error) {
console.error('Game initialization failed:', error);
setLoadingProgress('Error loading game. Please refresh.');
}
}
// Load user avatar from API or use default
async function loadUserAvatar() {
try {
// Try to get user profile from parent window (if in iframe)
if (window.parent && window.parent !== window) {
const message = await new Promise((resolve, reject) => {
const handler = (e) => {
if (e.data && e.data.type === 'avatarData') {
window.removeEventListener('message', handler);
resolve(e.data.avatar);
}
};
window.addEventListener('message', handler);
window.parent.postMessage({ type: 'requestAvatar' }, '*');
setTimeout(() => reject(new Error('Timeout')), 2000);
});
if (message && window.AvatarSystem) {
return AvatarSystem.importMinimal(message, message.userId || 'player');
}
}
} catch (e) {
// Fall through to default
}
// Return default avatar
if (window.AvatarSystem) {
return AvatarSystem.createDefault();
}
// Minimal fallback
return { base: { skinTone: '#FFD5B8' } };
}
// ═══════════════════════════════════════════════════════════════════════
// UTILITY FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════
// Update score
function updateScore(points) {
gameState.score += points;
document.getElementById('score').textContent = `Score: ${gameState.score}`;
// Notify parent window
if (window.parent !== window) {
window.parent.postMessage({ type: 'scoreUpdate', score: gameState.score }, '*');
}
}
// Update health
function updateHealth(amount) {
gameState.health = Math.max(0, Math.min(100, gameState.health + amount));
document.getElementById('health').textContent = `Health: ${gameState.health}`;
// Trigger hurt animation
if (amount < 0) {
setPlayerAnimation('hurt');
setTimeout(() => setPlayerAnimation('idle'), 500);
}
// Check for game over
if (gameState.health <= 0) {
endGame();
}
}
// Set player animation
function setPlayerAnimation(animationId) {
if (gameState.currentAnimation !== animationId) {
gameState.currentAnimation = animationId;
gameState.animationTime = 0;
if (window.ContextRenderer && gameState.player) {
ContextRenderer.animate(gameState.player, animationId);
}
}
}
// Play sound effect
function playSfx(name) {
if (!settings.sfxEnabled) return;
// HAIKU: Implement sound effects if needed
// Example: new Audio(`/sounds/${name}.mp3`).play();
}
// Random number helpers
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
// Collision detection
function rectCollision(r1, r2) {
return r1.x < r2.x + r2.width &&
r1.x + r1.width > r2.x &&
r1.y < r2.y + r2.height &&
r1.y + r1.height > r2.y;
}
function circleCollision(c1, c2) {
const dx = c1.x - c2.x;
const dy = c1.y - c2.y;
const dist = Math.sqrt(dx * dx + dy * dy);
return dist < c1.radius + c2.radius;
}
// Clamp value
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
// Lerp (linear interpolation)
function lerp(a, b, t) {
return a + (b - a) * t;
}
// ═══════════════════════════════════════════════════════════════════════
// GAME LIFECYCLE
// ═══════════════════════════════════════════════════════════════════════
function pauseGame() {
if (gameState.isGameOver) return;
gameState.isPaused = true;
document.getElementById('pauseMenu').style.display = 'block';
if (window.MusicEngine) {
MusicEngine.setVolume(settings.musicVolume * 0.3);
}
// Notify parent
if (window.pauseGame) window.pauseGame();
}
function resumeGame() {
gameState.isPaused = false;
document.getElementById('pauseMenu').style.display = 'none';
gameState.lastTime = performance.now();
if (window.MusicEngine) {
MusicEngine.setVolume(settings.musicVolume);
}
// Notify parent
if (window.resumeGame) window.resumeGame();
requestAnimationFrame(gameLoop);
}
function toggleMusic() {
settings.musicEnabled = !settings.musicEnabled;
const btn = document.querySelector('#pauseMenu button:nth-child(2)');
btn.textContent = `Music: ${settings.musicEnabled ? 'ON' : 'OFF'}`;
if (window.MusicEngine) {
if (settings.musicEnabled) {
MusicEngine.setVolume(settings.musicVolume);
} else {
MusicEngine.setVolume(0);
}
}
}
function toggleSfx() {
settings.sfxEnabled = !settings.sfxEnabled;
const btn = document.querySelector('#pauseMenu button:nth-child(3)');
btn.textContent = `Sound: ${settings.sfxEnabled ? 'ON' : 'OFF'}`;
}
function restartGame() {
// Reset state
gameState.score = 0;
gameState.health = 100;
gameState.level = 1;
gameState.isGameOver = false;
gameState.playerX = canvas.width / 2;
gameState.playerY = canvas.height / 2;
gameState.playerVelX = 0;
gameState.playerVelY = 0;
// Update UI
document.getElementById('score').textContent = 'Score: 0';
document.getElementById('health').textContent = 'Health: 100';
// Resume
resumeGame();
// Reinitialize game-specific state
initGame();
}
function endGame() {
gameState.isGameOver = true;
gameState.isRunning = false;
// Fade out music
if (window.MusicEngine) {
MusicEngine.fadeOut(2000);
}
// Report final score to parent
if (window.parent !== window) {
window.parent.postMessage({
type: 'gameOver',
score: gameState.score,
screenshot: captureScreenshot()
}, '*');
}
// Show game over (parent handles this, but fallback)
setTimeout(() => {
if (confirm(`Game Over!\n\nFinal Score: ${gameState.score}\n\nPlay again?`)) {
restartGame();
}
}, 500);
}
function captureScreenshot() {
return canvas.toDataURL('image/png');
}
// Expose functions for parent window control
window.pauseGame = pauseGame;
window.resumeGame = resumeGame;
window.pauseMusic = () => { if (window.MusicEngine) MusicEngine.setVolume(0); };
window.resumeMusic = () => { if (window.MusicEngine) MusicEngine.setVolume(settings.musicVolume); };
window.setMusicEnabled = (enabled) => { settings.musicEnabled = enabled; toggleMusic(); toggleMusic(); };
window.setSfxEnabled = (enabled) => { settings.sfxEnabled = enabled; };
window.gameScore = () => gameState.score;
// ═══════════════════════════════════════════════════════════════════════
// RENDERING
// ═══════════════════════════════════════════════════════════════════════
function renderBackground(time) {
const bg = GAME_CONFIG.assets.background;
if (window.BackgroundEngine) {
BackgroundEngine.render(
ctx,
bg.theme,
bg.variant,
canvas.width,
canvas.height,
time / 1000,
{ effects: bg.effects }
);
} else {
// Fallback: solid color
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
function renderPlayer() {
const config = GAME_CONFIG.assets.avatarContext;
const scale = config.scale || 1;
if (window.ContextRenderer && gameState.player) {
ContextRenderer.render(
gameState.player,
ctx,
gameState.playerX,
gameState.playerY,
{
scale: scale,
time: gameState.elapsedTime / 1000,
animation: gameState.currentAnimation,
frame: Math.floor(gameState.animationTime / 100) // ~10 fps animation
}
);
} else if (window.AvatarRenderer && gameState.player) {
// Fallback: render avatar directly
AvatarRenderer.render(
ctx,
gameState.player.avatar || gameState.player,
config.mode,
gameState.playerX,
gameState.playerY,
{ scale: scale }
);
} else {
// Minimal fallback: draw circle
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.arc(gameState.playerX, gameState.playerY, 30 * scale, 0, Math.PI * 2);
ctx.fill();
}
}
// ═══════════════════════════════════════════════════════════════════════
// GAME LOOP
// ═══════════════════════════════════════════════════════════════════════
function gameLoop(currentTime) {
if (!gameState.isRunning || gameState.isPaused) return;
// Calculate delta time
gameState.deltaTime = currentTime - gameState.lastTime;
gameState.lastTime = currentTime;
gameState.elapsedTime += gameState.deltaTime;
gameState.animationTime += gameState.deltaTime;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 1. Render background
renderBackground(gameState.elapsedTime);
// 2. Update game logic
// HAIKU: Call your update function here
updateGame(gameState.deltaTime / 1000); // Pass delta in seconds
// 3. Render game objects
// HAIKU: Call your render function here
renderGame();
// 4. Render player
renderPlayer();
// 5. Render UI overlay (if any canvas-based UI)
// HAIKU: Render any canvas-based UI here
renderUI();
// Continue loop
if (!gameState.isGameOver) {
requestAnimationFrame(gameLoop);
}
}
// ═══════════════════════════════════════════════════════════════════════
// INPUT HANDLING
// ═══════════════════════════════════════════════════════════════════════
// Track pressed keys
const keys = {};
document.addEventListener('keydown', (e) => {
keys[e.code] = true;
// Pause on Escape
if (e.code === 'Escape') {
if (gameState.isPaused) resumeGame();
else pauseGame();
return;
}
// HAIKU: Handle game-specific key presses
handleKeyDown(e.code);
});
document.addEventListener('keyup', (e) => {
keys[e.code] = false;
// HAIKU: Handle game-specific key releases
handleKeyUp(e.code);
});
// Touch/mouse input
let touchStartX = 0, touchStartY = 0;
let isTouching = false;
canvas.addEventListener('mousedown', (e) => {
touchStartX = e.clientX;
touchStartY = e.clientY;
isTouching = true;
handleTouchStart(e.clientX, e.clientY);
});
canvas.addEventListener('mousemove', (e) => {
if (isTouching) {
handleTouchMove(e.clientX, e.clientY, e.clientX - touchStartX, e.clientY - touchStartY);
}
handleMouseMove(e.clientX, e.clientY);
});
canvas.addEventListener('mouseup', (e) => {
isTouching = false;
handleTouchEnd(e.clientX, e.clientY);
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
isTouching = true;
handleTouchStart(touch.clientX, touch.clientY);
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
handleTouchMove(touch.clientX, touch.clientY, touch.clientX - touchStartX, touch.clientY - touchStartY);
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
isTouching = false;
handleTouchEnd(touchStartX, touchStartY);
});
// ═══════════════════════════════════════════════════════════════════════
// GAME-SPECIFIC CODE
// ═══════════════════════════════════════════════════════════════════════
// HAIKU: Implement the functions below with your game logic
/**
* Initialize game-specific state
* Called once at start and on restart
*/
function initGame() {
// HAIKU: Initialize your game objects, enemies, items, etc.
// Example:
// gameState.enemies = [];
// gameState.items = [];
// spawnInitialObjects();
setPlayerAnimation('idle');
}
/**
* Update game logic
* @param {number} dt - Delta time in seconds
*/
function updateGame(dt) {
// HAIKU: Update your game objects here
// Example:
// updateEnemies(dt);
// updateItems(dt);
// checkCollisions();
// spawnNewObjects();
// Example: Continuous key input for movement
// if (keys['ArrowLeft'] || keys['KeyA']) gameState.playerX -= 300 * dt;
// if (keys['ArrowRight'] || keys['KeyD']) gameState.playerX += 300 * dt;
// if (keys['ArrowUp'] || keys['KeyW']) gameState.playerY -= 300 * dt;
// if (keys['ArrowDown'] || keys['KeyS']) gameState.playerY += 300 * dt;
// Keep player in bounds
// gameState.playerX = clamp(gameState.playerX, 50, canvas.width - 50);
// gameState.playerY = clamp(gameState.playerY, 50, canvas.height - 50);
}
/**
* Render game objects (not player, not background)
*/
function renderGame() {
// HAIKU: Render your game objects here
// Example:
// gameState.enemies.forEach(enemy => renderEnemy(enemy));
// gameState.items.forEach(item => renderItem(item));
}
/**
* Render any canvas-based UI (health bars, indicators, etc.)
*/
function renderUI() {
// HAIKU: Render canvas-based UI elements
// Example health bar:
// const barWidth = 200;
// const healthPct = gameState.health / 100;
// ctx.fillStyle = '#333';
// ctx.fillRect(canvas.width - barWidth - 20, 20, barWidth, 20);
// ctx.fillStyle = healthPct > 0.3 ? '#4CAF50' : '#f44336';
// ctx.fillRect(canvas.width - barWidth - 20, 20, barWidth * healthPct, 20);
}
/**
* Handle key down event
* @param {string} code - Key code (e.g., 'Space', 'ArrowUp')
*/
function handleKeyDown(code) {
// HAIKU: Handle instant key actions (jump, shoot, etc.)
// Example:
// if (code === 'Space') jump();
// if (code === 'KeyX') shoot();
}
/**
* Handle key up event
* @param {string} code - Key code
*/
function handleKeyUp(code) {
// HAIKU: Handle key release actions
}
/**
* Handle touch/click start
* @param {number} x - X position
* @param {number} y - Y position
*/
function handleTouchStart(x, y) {
// HAIKU: Handle touch/click start
// Example: Check if clicked on a button, start drag, etc.
}
/**
* Handle touch/mouse move
* @param {number} x - Current X
* @param {number} y - Current Y
* @param {number} dx - Delta X from start
* @param {number} dy - Delta Y from start
*/
function handleTouchMove(x, y, dx, dy) {
// HAIKU: Handle drag/swipe
// Example: gameState.playerX = x;
}
/**
* Handle touch/click end
* @param {number} x - X position
* @param {number} y - Y position
*/
function handleTouchEnd(x, y) {
// HAIKU: Handle touch/click end
}
/**
* Handle mouse move (hover, not dragging)
* @param {number} x - X position
* @param {number} y - Y position
*/
function handleMouseMove(x, y) {
// HAIKU: Handle mouse hover
// Example: Aim at cursor
}
// ═══════════════════════════════════════════════════════════════════════
// START GAME
// ═══════════════════════════════════════════════════════════════════════
// Wait for DOM then initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
</body>
</html>
@@ -0,0 +1,392 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><!-- GAME_TITLE --></title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
overflow: hidden;
background: #1a1a2e;
touch-action: none;
}
canvas { display: block; }
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: Arial, sans-serif;
font-size: 18px;
text-shadow: 2px 2px 4px #000;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">Score: <span id="score">0</span></div>
<script>
// ═══════════════════════════════════════════════════════════════════════
// SIMPLE GAME TEMPLATE
// ═══════════════════════════════════════════════════════════════════════
// This is a minimal template without the asset library.
// Use for simple games that don't need avatars, procedural music, etc.
//
// HAIKU: Fill in GAME_CONFIG and implement the game functions below.
const GAME_CONFIG = {
title: "<!-- GAME_TITLE -->",
// Background color or gradient
background: {
type: "gradient", // "solid", "gradient", or "pattern"
colors: ["#1a1a2e", "#16213e", "#0f3460"], // gradient colors
// color: "#1a1a2e" // for solid
},
// Player settings
player: {
width: 40,
height: 40,
color: "#4CAF50",
speed: 300 // pixels per second
},
// Game settings
// HAIKU: Add game-specific config here
gameplay: {
// <!-- GAMEPLAY_CONFIG -->
}
};
// ═══════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Resize to fill screen
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// Game state
let state = {
// Player position
playerX: 0,
playerY: 0,
playerVelX: 0,
playerVelY: 0,
// Score and status
score: 0,
gameOver: false,
paused: false,
// Timing
lastTime: 0,
// HAIKU: Add game-specific state
// <!-- GAME_STATE -->
};
// Input tracking
const keys = {};
let mouseX = 0, mouseY = 0;
let mouseDown = false;
// ═══════════════════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════════════════
function updateScore(points) {
state.score += points;
document.getElementById('score').textContent = state.score;
}
function endGame() {
state.gameOver = true;
if (window.parent !== window) {
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
}
setTimeout(() => {
if (confirm(`Game Over! Score: ${state.score}\n\nPlay again?`)) {
restart();
}
}, 100);
}
function restart() {
state.score = 0;
state.gameOver = false;
state.playerX = canvas.width / 2;
state.playerY = canvas.height / 2;
document.getElementById('score').textContent = '0';
initGame();
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
function rectCollide(r1, r2) {
return r1.x < r2.x + r2.w && r1.x + r1.w > r2.x &&
r1.y < r2.y + r2.h && r1.y + r1.h > r2.y;
}
function circleCollide(c1, c2) {
return distance(c1.x, c1.y, c2.x, c2.y) < c1.r + c2.r;
}
// ═══════════════════════════════════════════════════════════════════════
// RENDERING HELPERS
// ═══════════════════════════════════════════════════════════════════════
function drawBackground() {
const bg = GAME_CONFIG.background;
if (bg.type === 'gradient' && bg.colors) {
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
bg.colors.forEach((color, i) => {
gradient.addColorStop(i / (bg.colors.length - 1), color);
});
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = bg.color || '#1a1a2e';
}
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawPlayer() {
const p = GAME_CONFIG.player;
ctx.fillStyle = p.color;
ctx.fillRect(
state.playerX - p.width / 2,
state.playerY - p.height / 2,
p.width,
p.height
);
}
function drawRect(x, y, w, h, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, w, h);
}
function drawCircle(x, y, r, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
function drawText(text, x, y, color, size, align) {
ctx.fillStyle = color || 'white';
ctx.font = `${size || 20}px Arial`;
ctx.textAlign = align || 'left';
ctx.fillText(text, x, y);
}
// ═══════════════════════════════════════════════════════════════════════
// INPUT HANDLING
// ═══════════════════════════════════════════════════════════════════════
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'Escape') {
state.paused = !state.paused;
if (!state.paused) requestAnimationFrame(gameLoop);
}
onKeyDown(e.code);
});
document.addEventListener('keyup', e => {
keys[e.code] = false;
onKeyUp(e.code);
});
canvas.addEventListener('mousedown', e => {
mouseDown = true;
mouseX = e.clientX;
mouseY = e.clientY;
onMouseDown(mouseX, mouseY);
});
canvas.addEventListener('mousemove', e => {
mouseX = e.clientX;
mouseY = e.clientY;
onMouseMove(mouseX, mouseY);
});
canvas.addEventListener('mouseup', e => {
mouseDown = false;
onMouseUp(e.clientX, e.clientY);
});
canvas.addEventListener('touchstart', e => {
e.preventDefault();
mouseDown = true;
mouseX = e.touches[0].clientX;
mouseY = e.touches[0].clientY;
onMouseDown(mouseX, mouseY);
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
mouseX = e.touches[0].clientX;
mouseY = e.touches[0].clientY;
onMouseMove(mouseX, mouseY);
}, { passive: false });
canvas.addEventListener('touchend', e => {
mouseDown = false;
onMouseUp(mouseX, mouseY);
});
// ═══════════════════════════════════════════════════════════════════════
// GAME LOOP
// ═══════════════════════════════════════════════════════════════════════
function gameLoop(time) {
if (state.gameOver || state.paused) return;
const dt = (time - state.lastTime) / 1000;
state.lastTime = time;
// Update
update(dt);
// Render
drawBackground();
render();
drawPlayer();
renderUI();
requestAnimationFrame(gameLoop);
}
// ═══════════════════════════════════════════════════════════════════════
// GAME LOGIC - HAIKU IMPLEMENTS THESE
// ═══════════════════════════════════════════════════════════════════════
/**
* Initialize game state
*/
function initGame() {
state.playerX = canvas.width / 2;
state.playerY = canvas.height / 2;
// HAIKU: Initialize your game objects
// Example:
// state.enemies = [];
// state.items = [];
}
/**
* Update game logic each frame
* @param {number} dt - Delta time in seconds
*/
function update(dt) {
// HAIKU: Implement game update logic
// Example: Arrow key movement
const speed = GAME_CONFIG.player.speed;
if (keys['ArrowLeft'] || keys['KeyA']) state.playerX -= speed * dt;
if (keys['ArrowRight'] || keys['KeyD']) state.playerX += speed * dt;
if (keys['ArrowUp'] || keys['KeyW']) state.playerY -= speed * dt;
if (keys['ArrowDown'] || keys['KeyS']) state.playerY += speed * dt;
// Keep in bounds
const hw = GAME_CONFIG.player.width / 2;
const hh = GAME_CONFIG.player.height / 2;
state.playerX = clamp(state.playerX, hw, canvas.width - hw);
state.playerY = clamp(state.playerY, hh, canvas.height - hh);
}
/**
* Render game objects (not player, not background)
*/
function render() {
// HAIKU: Render your game objects
// Example:
// state.enemies.forEach(e => drawCircle(e.x, e.y, e.r, e.color));
}
/**
* Render UI elements on canvas
*/
function renderUI() {
// HAIKU: Render any additional UI
}
/**
* Handle key press
*/
function onKeyDown(code) {
// HAIKU: Handle key press events
// if (code === 'Space') jump();
}
/**
* Handle key release
*/
function onKeyUp(code) {
// HAIKU: Handle key release events
}
/**
* Handle mouse/touch down
*/
function onMouseDown(x, y) {
// HAIKU: Handle click/tap
}
/**
* Handle mouse/touch move
*/
function onMouseMove(x, y) {
// HAIKU: Handle mouse/touch movement
}
/**
* Handle mouse/touch up
*/
function onMouseUp(x, y) {
// HAIKU: Handle click/tap release
}
// ═══════════════════════════════════════════════════════════════════════
// START
// ═══════════════════════════════════════════════════════════════════════
// Expose for parent window
window.pauseGame = () => { state.paused = true; };
window.resumeGame = () => { state.paused = false; state.lastTime = performance.now(); requestAnimationFrame(gameLoop); };
window.gameScore = state.score;
// Initialize and start
initGame();
state.lastTime = performance.now();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+275
View File
@@ -0,0 +1,275 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #f8fafc;
color: #1e293b;
-webkit-font-smoothing: antialiased;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
border: none;
border-radius: 0.5rem;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}
.btn-secondary {
background: #e2e8f0;
color: #475569;
}
.btn-secondary:hover:not(:disabled) {
background: #cbd5e1;
}
.btn-danger {
background: #dc2626;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #b91c1c;
}
.btn-sm {
padding: 0.4rem 0.875rem;
font-size: 0.85rem;
}
.btn-lg {
padding: 0.875rem 1.75rem;
font-size: 1.1rem;
}
/* Form elements */
input, textarea, select {
font-family: inherit;
}
/* Links */
a {
color: #6366f1;
}
/* Utility */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 4rem;
color: #475569; /* WCAG AA: 7.0:1 contrast on #f8fafc */
}
/* ============================================
Accessibility - Focus Styles
WCAG 2.1 Level AA compliant keyboard navigation
============================================ */
/* Global focus-visible styles for all interactive elements */
:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* Remove default focus for mouse users, keep for keyboard */
:focus:not(:focus-visible) {
outline: none;
}
/* Button focus states */
.btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2);
}
.btn-primary:focus-visible {
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.3);
}
.btn-secondary:focus-visible {
outline-color: #475569;
box-shadow: 0 0 0 4px rgba(71, 85, 105, 0.2);
}
.btn-danger:focus-visible {
outline-color: #dc2626;
box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.2);
}
/* Link focus states */
a:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
border-radius: 2px;
}
/* Form element focus states */
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 0;
border-color: #6366f1;
}
/* Skip to content link - hidden until focused */
.skip-to-content {
position: absolute;
top: -100%;
left: 50%;
transform: translateX(-50%);
background: #6366f1;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0 0 0.5rem 0.5rem;
font-weight: 600;
z-index: 10000;
text-decoration: none;
transition: top 0.2s ease;
}
.skip-to-content:focus {
top: 0;
outline: 2px solid white;
outline-offset: 2px;
}
/* ============================================
Mobile-First Global Styles
============================================ */
/* Prevent horizontal overflow - only on body to preserve sticky on html */
body {
overflow-x: hidden;
max-width: 100vw;
}
html {
/* Allow sticky to work properly */
overflow-x: clip; /* Modern alternative that doesn't break sticky */
}
/* Mobile text sizes - prevent iOS zoom on focus */
@media (max-width: 768px) {
input, textarea, select {
font-size: 16px !important;
}
.btn {
min-height: 44px;
padding: 0.75rem 1rem;
}
.btn-sm {
min-height: 40px;
padding: 0.5rem 0.75rem;
}
}
/* Touch-friendly targets */
@media (hover: none), (pointer: coarse) {
.btn,
button,
a,
input[type="checkbox"],
input[type="radio"] {
min-height: 44px;
min-width: 44px;
}
/* Ensure form inputs are easy to tap */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="url"],
textarea,
select {
min-height: 48px;
padding: 0.75rem 1rem;
}
/* Disable hover effects on touch */
.btn:hover {
transform: none;
}
}
/* Active states for touch devices */
@media (hover: none) {
.btn:active {
transform: scale(0.98);
}
.btn-primary:active {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
}
.btn-secondary:active {
background: #94a3b8;
}
}
/* Safe area padding for notched devices */
@supports (padding: max(0px)) {
.header {
padding-left: max(1rem, env(safe-area-inset-left));
padding-right: max(1rem, env(safe-area-inset-right));
}
.main-content {
padding-bottom: max(0px, env(safe-area-inset-bottom));
}
}
/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
+108
View File
@@ -0,0 +1,108 @@
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import { AuthProvider, useAuth } from './context/AuthContext';
import Header from './components/Header';
// Scroll to top on every route change
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
import Footer from './components/Footer';
import InstallPrompt from './components/InstallPrompt';
import OfflineIndicator from './components/OfflineIndicator';
import OnboardingTutorial from './components/OnboardingTutorial';
import Home from './pages/Home';
import Arcade from './pages/Arcade';
import Play from './pages/Play';
import Login from './pages/Login';
import Register from './pages/Register';
import ForgotPassword from './pages/ForgotPassword';
import ResetPassword from './pages/ResetPassword';
import CreateWizard from './pages/CreateWizard';
import Edit from './pages/Edit';
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import Collections from './pages/Collections';
import Admin from './pages/Admin';
import Terms from './pages/Terms';
import Privacy from './pages/Privacy';
import Achievements from './pages/Achievements';
import Classrooms from './pages/Classrooms';
import TeacherConsole from './pages/TeacherConsole';
import GameAnalytics from './pages/GameAnalytics';
import Leaderboard from './pages/Leaderboard';
import CreatorProfile from './pages/CreatorProfile';
import GameStats from './pages/GameStats';
import ReleaseNotes from './pages/ReleaseNotes';
import './App.css';
function AppContent() {
const { user, loading, isAuthenticated, completeOnboarding } = useAuth();
// Show onboarding tutorial for new authenticated users who haven't completed it
// Wait for loading to finish to prevent flash of onboarding
const showOnboarding = !loading && isAuthenticated && user && !user.onboardingComplete;
return (
<>
<OfflineIndicator />
<Header />
<InstallPrompt />
{showOnboarding && (
<OnboardingTutorial onComplete={completeOnboarding} />
)}
<main id="main-content" className="main-content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/arcade" element={<Arcade />} />
<Route path="/play/:gameId" element={<Play />} />
<Route path="/game/:gameId/stats" element={<GameStats />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/create" element={<CreateWizard />} />
<Route path="/edit/:gameId" element={<Edit />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/profile/:userId" element={<Profile />} />
<Route path="/creator/:username" element={<CreatorProfile />} />
<Route path="/collections" element={<Collections />} />
<Route path="/collections/:collectionId" element={<Collections />} />
<Route path="/admin" element={<Admin />} />
<Route path="/terms" element={<Terms />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/achievements" element={<Achievements />} />
<Route path="/classrooms" element={<Classrooms />} />
<Route path="/classrooms/:classroomId" element={<Classrooms />} />
<Route path="/teacher" element={<TeacherConsole />} />
<Route path="/analytics/:gameId" element={<GameAnalytics />} />
<Route path="/leaderboard" element={<Leaderboard />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
</Routes>
</main>
<Footer />
</>
);
}
function App() {
return (
<AuthProvider>
<BrowserRouter>
<ScrollToTop />
<div className="app">
<AppContent />
</div>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
+785
View File
@@ -0,0 +1,785 @@
import { getFingerprint } from './hooks/useFingerprint';
const API_BASE = '/api';
class ApiClient {
constructor() {
this.token = localStorage.getItem('token');
}
setToken(token) {
this.token = token;
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
}
async request(endpoint, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
// Add fingerprint header for guest authentication
const fingerprint = getFingerprint();
if (fingerprint) {
headers['X-Device-Fingerprint'] = fingerprint;
}
// Support custom timeout (default 30s, long requests can specify more)
const timeout = options.timeout || 30000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await response.json();
if (!response.ok) {
// Check if this is a guest upgrade prompt
if (data.requiresSignup) {
const error = new Error(data.message || 'Sign up required');
error.requiresSignup = true;
throw error;
}
throw new Error(data.error || 'Request failed');
}
return data;
} catch (err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw new Error('Request timed out. Please try again.');
}
throw err;
}
}
// Auth
async register(username, email, password) {
const fingerprint = getFingerprint();
const data = await this.request('/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password, fingerprint })
});
this.setToken(data.token);
return data;
}
async login(email, password) {
const data = await this.request('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
this.setToken(data.token);
return data;
}
async guestLogin(fingerprint) {
const data = await this.request('/auth/guest-login', {
method: 'POST',
body: JSON.stringify({ fingerprint })
});
this.setToken(data.token);
return data;
}
async forgotPassword(email) {
return this.request('/auth/forgot-password', {
method: 'POST',
body: JSON.stringify({ email })
});
}
async resetPassword(token, password) {
return this.request('/auth/reset-password', {
method: 'POST',
body: JSON.stringify({ token, password })
});
}
logout() {
this.setToken(null);
}
// User
async getMe() {
return this.request('/users/me');
}
async updateProfile(data) {
return this.request('/users/me', {
method: 'PUT',
body: JSON.stringify(data)
});
}
async updateAvatar(avatarData) {
return this.request('/users/me/avatar', {
method: 'PUT',
body: JSON.stringify({ avatar: avatarData })
});
}
async getPlayHistory() {
return this.request('/users/me/play-history');
}
async getMyStats() {
return this.request('/users/me/stats');
}
async getMyAchievements() {
return this.request('/users/me/achievements');
}
async getAllAchievements() {
return this.request('/users/me/achievements?all=true');
}
async getMyFavorites() {
return this.request('/users/me/favorites');
}
async getXPHistory(limit = 20) {
return this.request(`/users/me/xp-history?limit=${limit}`);
}
async getMyCollections() {
return this.request('/users/me/collections');
}
async getMyGames() {
return this.request('/users/me/games');
}
async getNotifications(unreadOnly = false) {
return this.request(`/users/me/notifications${unreadOnly ? '?unread=true' : ''}`);
}
async markNotificationRead(notificationId) {
return this.request(`/users/me/notifications/${notificationId}/read`, {
method: 'POST'
});
}
async markAllNotificationsRead() {
return this.request('/users/me/notifications/read-all', {
method: 'POST'
});
}
// Profiles
async getUserProfile(userId) {
return this.request(`/users/${userId}`);
}
async getCreatorByUsername(username) {
return this.request(`/users/by-username/${encodeURIComponent(username)}`);
}
async getCreatorGames(username) {
return this.request(`/users/by-username/${encodeURIComponent(username)}/games`);
}
async followUser(userId) {
return this.request(`/users/${userId}/follow`, {
method: 'POST'
});
}
async unfollowUser(userId) {
return this.request(`/users/${userId}/follow`, {
method: 'DELETE'
});
}
async getFollowers(userId) {
return this.request(`/users/${userId}/followers`);
}
async getFollowing(userId) {
return this.request(`/users/${userId}/following`);
}
// Arcade
async getGames(params = {}) {
const query = new URLSearchParams(params).toString();
return this.request(`/arcade/games${query ? `?${query}` : ''}`);
}
async getGame(gameId) {
return this.request(`/arcade/games/${gameId}`);
}
async getHighScores(gameId) {
return this.request(`/arcade/games/${gameId}/high-scores`);
}
// Play
async startPlay(gameId) {
const fingerprint = getFingerprint();
return this.request('/play/start', {
method: 'POST',
body: JSON.stringify({ gameId, fingerprint })
});
}
async finishPlay(sessionId, score, durationSeconds, levelsCompleted = 0) {
return this.request('/play/finish', {
method: 'POST',
body: JSON.stringify({ sessionId, score, durationSeconds, levelsCompleted })
});
}
async rateGame(gameId, rating) {
return this.request('/play/rate', {
method: 'POST',
body: JSON.stringify({ gameId, rating })
});
}
async toggleFavorite(gameId) {
return this.request('/play/favorite', {
method: 'POST',
body: JSON.stringify({ gameId })
});
}
// Game Creation Wizard
async getGameStyles() {
return this.request('/creation/styles');
}
async getStyleQuestions(styleId) {
return this.request(`/creation/styles/${styleId}/questions`);
}
async estimateComplexity(styleId, answers) {
return this.request('/creation/estimate-complexity', {
method: 'POST',
body: JSON.stringify({ styleId, answers })
});
}
async startCreation(styleId, title, answers) {
return this.request('/creation/start', {
method: 'POST',
body: JSON.stringify({ styleId, title, answers })
});
}
async generateGame(gameId, editedPrompt = null, promptWasEdited = false) {
// Long timeout for AI generation (180 seconds to match nginx)
return this.request(`/creation/${gameId}/generate`, {
method: 'POST',
timeout: 180000,
body: JSON.stringify({
editedPrompt,
promptWasEdited
})
});
}
async trackTesting(gameId, durationSeconds, levelsCompleted = 0, thumbnailData = null) {
return this.request(`/creation/${gameId}/test`, {
method: 'POST',
body: JSON.stringify({ durationSeconds, levelsCompleted, thumbnailData })
});
}
async saveThumbnail(gameId, thumbnailData) {
return this.request(`/creation/${gameId}/thumbnail`, {
method: 'POST',
body: JSON.stringify({ thumbnailData })
});
}
async refineGame(gameId, feedback) {
// Long timeout for AI refinement (180 seconds to match nginx)
return this.request(`/creation/${gameId}/refine`, {
method: 'POST',
body: JSON.stringify({ feedback }),
timeout: 180000
});
}
// Local AI endpoints
async refinePromptChat(gameIdea, conversationHistory) {
return this.request('/localai/refine-prompt', {
method: 'POST',
body: JSON.stringify({ gameIdea, conversationHistory }),
timeout: 150000
});
}
async classifyFeedback(gameId, feedback) {
return this.request('/localai/classify', {
method: 'POST',
body: JSON.stringify({ gameId, feedback }),
timeout: 30000
});
}
async previewTweak(gameId, feedback) {
return this.request(`/localai/tweak/${gameId}/preview`, {
method: 'POST',
body: JSON.stringify({ feedback }),
timeout: 30000
});
}
async applyFreeTweak(gameId, feedback) {
return this.request(`/localai/tweak/${gameId}`, {
method: 'POST',
body: JSON.stringify({ feedback }),
timeout: 150000
});
}
async reviewCodeChanges(gameId, newCode) {
return this.request(`/localai/review-code/${gameId}`, {
method: 'POST',
body: JSON.stringify({ newCode }),
timeout: 30000
});
}
async saveGameCode(gameId, newCode) {
return this.request(`/localai/save-code/${gameId}`, {
method: 'POST',
body: JSON.stringify({ newCode }),
timeout: 30000
});
}
async publishGame(gameId, metadata = {}) {
return this.request(`/creation/${gameId}/publish`, {
method: 'POST',
body: JSON.stringify(metadata)
});
}
async getCreationStatus(gameId) {
return this.request(`/creation/${gameId}/status`);
}
async getGameVersions(gameId) {
return this.request(`/creation/${gameId}/versions`);
}
async rollbackGame(gameId, versionNumber) {
return this.request(`/creation/${gameId}/rollback`, {
method: 'POST',
body: JSON.stringify({ versionNumber })
});
}
async remixGame(gameId) {
return this.request(`/creation/${gameId}/remix`, {
method: 'POST'
});
}
// Legacy game creation (old system)
async createGame(description, title) {
return this.request('/games/create', {
method: 'POST',
body: JSON.stringify({ description, title })
});
}
async getMyGame(gameId) {
return this.request(`/games/${gameId}`);
}
async updateGame(gameId, data) {
return this.request(`/games/${gameId}`, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async submitGame(gameId) {
return this.request(`/games/${gameId}/submit`, {
method: 'POST'
});
}
async unpublishGame(gameId) {
return this.request(`/games/${gameId}/unpublish`, {
method: 'POST'
});
}
async deleteGame(gameId) {
return this.request(`/games/${gameId}`, {
method: 'DELETE'
});
}
// Collections
async createCollection(name, description = '', isPublic = true) {
return this.request('/collections', {
method: 'POST',
body: JSON.stringify({ name, description, isPublic })
});
}
async getCollection(collectionId) {
return this.request(`/collections/${collectionId}`);
}
async updateCollection(collectionId, data) {
return this.request(`/collections/${collectionId}`, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async deleteCollection(collectionId) {
return this.request(`/collections/${collectionId}`, {
method: 'DELETE'
});
}
async addToCollection(collectionId, gameId) {
return this.request(`/collections/${collectionId}/games`, {
method: 'POST',
body: JSON.stringify({ gameId })
});
}
async removeFromCollection(collectionId, gameId) {
return this.request(`/collections/${collectionId}/games/${gameId}`, {
method: 'DELETE'
});
}
// Developer
async getDashboard() {
return this.request('/developer/dashboard');
}
async getMyGames() {
return this.request('/developer/games');
}
async getGameAnalytics(gameId) {
return this.request(`/developer/games/${gameId}/analytics`);
}
// Report game
async reportGame(gameId, reason, comment) {
return this.request('/play/report', {
method: 'POST',
body: JSON.stringify({ gameId, reason, comment })
});
}
// Admin endpoints
async getModerationQueue() {
return this.request('/admin/moderation-queue');
}
async reviewGame(gameId, action, reason = null) {
return this.request(`/admin/review/${gameId}`, {
method: 'POST',
body: JSON.stringify({ action, reason })
});
}
async getReports() {
return this.request('/admin/reports');
}
async resolveReport(reportId, action, notes = null) {
return this.request(`/admin/reports/${reportId}/resolve`, {
method: 'POST',
body: JSON.stringify({ action, notes })
});
}
async getAdminStats() {
return this.request('/admin/stats');
}
async searchGameDiagnostics(search = '', gameId = '') {
const params = new URLSearchParams();
if (search) params.set('search', search);
if (gameId) params.set('gameId', gameId);
return this.request(`/admin/game-diagnostics?${params.toString()}`);
}
async banUser(userId) {
return this.request(`/admin/ban-user/${userId}`, {
method: 'POST'
});
}
async adminUnpublishGame(gameId, reason) {
return this.request(`/admin/unpublish/${gameId}`, {
method: 'POST',
body: JSON.stringify({ reason })
});
}
async clearCloudflareCache() {
return this.request('/admin/clear-cache', {
method: 'POST'
});
}
async getSystemHealth() {
return this.request('/admin/system-health');
}
// Share
async getShareData(gameId) {
return this.request(`/share/data/${gameId}`);
}
// Themes
async getCurrentTheme() {
return this.request('/themes/current');
}
async getUpcomingThemes() {
return this.request('/themes/upcoming');
}
async getPastThemes() {
return this.request('/themes/past');
}
// Classrooms
async getMyClassrooms() {
return this.request('/classrooms/my');
}
async createClassroom(name, isPublicPublishingAllowed = false) {
return this.request('/classrooms', {
method: 'POST',
body: JSON.stringify({ name, isPublicPublishingAllowed })
});
}
async getClassroom(classroomId) {
return this.request(`/classrooms/${classroomId}`);
}
async updateClassroom(classroomId, data) {
return this.request(`/classrooms/${classroomId}`, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async deleteClassroom(classroomId) {
return this.request(`/classrooms/${classroomId}`, {
method: 'DELETE'
});
}
async regenerateJoinCode(classroomId) {
return this.request(`/classrooms/${classroomId}/regenerate-code`, {
method: 'POST'
});
}
async joinClassroom(joinCode) {
return this.request('/classrooms/join', {
method: 'POST',
body: JSON.stringify({ joinCode })
});
}
async leaveClassroom(classroomId) {
return this.request(`/classrooms/${classroomId}/leave`, {
method: 'POST'
});
}
async getClassroomStudents(classroomId) {
return this.request(`/classrooms/${classroomId}/students`);
}
async removeStudentFromClassroom(classroomId, studentId) {
return this.request(`/classrooms/${classroomId}/students/${studentId}`, {
method: 'DELETE'
});
}
async getClassroomLeaderboard(classroomId) {
return this.request(`/classrooms/${classroomId}/leaderboard`);
}
async getClassroomAssignments(classroomId) {
return this.request(`/classrooms/${classroomId}/assignments`);
}
async createAssignment(classroomId, data) {
return this.request(`/classrooms/${classroomId}/assignments`, {
method: 'POST',
body: JSON.stringify(data)
});
}
async deleteAssignment(classroomId, assignmentId) {
return this.request(`/classrooms/${classroomId}/assignments/${assignmentId}`, {
method: 'DELETE'
});
}
async getClassroomStudentProgress(classroomId) {
return this.request(`/classrooms/${classroomId}/student-progress`);
}
async getMyAssignments(classroomId) {
return this.request(`/classrooms/${classroomId}/my-assignments`);
}
async completeAssignment(classroomId, assignmentId, data = {}) {
return this.request(`/classrooms/${classroomId}/assignments/${assignmentId}/complete`, {
method: 'POST',
body: JSON.stringify(data)
});
}
async getAssignmentCompletions(classroomId, assignmentId) {
return this.request(`/classrooms/${classroomId}/assignments/${assignmentId}/completions`);
}
async broadcastToClassroom(classroomId, message, title = '') {
return this.request(`/classrooms/${classroomId}/broadcast`, {
method: 'POST',
body: JSON.stringify({ message, title })
});
}
// Leaderboard
async getTopPlayers(page = 1, limit = 10) {
return this.request(`/leaderboard/players?page=${page}&limit=${limit}`);
}
async getTopCreators(page = 1, limit = 10, period = 'all') {
return this.request(`/leaderboard/creators?page=${page}&limit=${limit}&period=${period}`);
}
async getTopGames(page = 1, limit = 10, period = 'all') {
return this.request(`/leaderboard/games?page=${page}&limit=${limit}&period=${period}`);
}
// Onboarding
async getOnboardingStatus() {
return this.request('/users/me/onboarding');
}
async updateOnboardingStep(step) {
return this.request('/users/me/onboarding', {
method: 'POST',
body: JSON.stringify({ step })
});
}
async completeOnboarding() {
return this.request('/users/me/onboarding', {
method: 'POST',
body: JSON.stringify({ complete: true })
});
}
// Contact
async submitContact(name, email, subject, message) {
return this.request('/contact', {
method: 'POST',
body: JSON.stringify({ name, email, subject, message })
});
}
// SSE Streaming for generation/refinement
streamGeneration(gameId, onEvent) {
const token = this.token;
const url = `${API_BASE}/creation/${gameId}/stream?token=${encodeURIComponent(token)}`;
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
onEvent(data);
} catch {
// Ignore parse errors
}
};
eventSource.onerror = () => {
// EventSource will auto-reconnect; caller can close if needed
};
return eventSource;
}
// Push notification methods
async getVapidKey() {
return this.request('/push/vapid-key');
}
async subscribePush(subscription) {
return this.request('/push/subscribe', {
method: 'POST',
body: JSON.stringify({ subscription })
});
}
async unsubscribePush(endpoint) {
return this.request('/push/unsubscribe', {
method: 'POST',
body: JSON.stringify({ endpoint })
});
}
// Bug Reports
async submitBugReport(title, description, pageUrl = null) {
return this.request('/bugs', {
method: 'POST',
body: JSON.stringify({ title, description, pageUrl })
});
}
async getMyBugReports() {
return this.request('/bugs/my-reports');
}
async getAdminBugReports(status = 'all', priority = 'all') {
return this.request(`/bugs/admin?status=${status}&priority=${priority}`);
}
async updateBugReport(id, data) {
return this.request(`/bugs/admin/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async deleteBugReport(id) {
return this.request(`/bugs/admin/${id}`, {
method: 'DELETE'
});
}
}
export const api = new ApiClient();
export default api;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+214
View File
@@ -0,0 +1,214 @@
.avatar-builder {
background: white;
border-radius: 1rem;
padding: 1.5rem;
max-width: 500px;
margin: 0 auto;
}
.avatar-builder-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
.avatar-builder-preview .avatar-svg {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
.randomize-btn {
display: flex;
align-items: center;
gap: 0.35rem;
}
.avatar-builder-controls {
margin-bottom: 1.5rem;
}
.category-tabs {
display: flex;
gap: 0.25rem;
margin-bottom: 1rem;
overflow-x: auto;
padding-bottom: 0.5rem;
-webkit-overflow-scrolling: touch;
}
.category-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 0.75rem;
border: none;
background: #f1f5f9;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
min-width: 60px;
flex-shrink: 0;
}
.category-tab:hover {
background: #e2e8f0;
}
.category-tab.active {
background: #6366f1;
color: white;
}
.tab-icon {
font-size: 1.25rem;
}
.tab-label {
font-size: 0.65rem;
font-weight: 500;
white-space: nowrap;
}
.options-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 0.5rem;
}
.option-btn {
padding: 0.75rem 0.5rem;
border: 2px solid #e2e8f0;
background: white;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
font-weight: 500;
color: #475569;
position: relative;
}
.option-btn:hover {
border-color: #6366f1;
}
.option-btn.selected {
border-color: #6366f1;
background: #ede9fe;
color: #4f46e5;
}
.option-btn.color-option {
aspect-ratio: 1;
border-radius: 50%;
padding: 0;
min-height: 45px;
}
.option-btn.color-option.selected {
box-shadow: 0 0 0 3px white, 0 0 0 5px #6366f1;
}
.option-btn .check {
position: absolute;
top: 2px;
right: 4px;
font-size: 0.75rem;
color: #6366f1;
}
.option-btn.color-option .check {
top: 50%;
left: 50%;
right: auto;
transform: translate(-50%, -50%);
color: white;
font-size: 1rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.avatar-builder-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
/* Avatar SVG styling */
.avatar-svg {
display: block;
overflow: visible;
}
/* Mini placeholder */
.avatar-mini-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
border-radius: 50%;
}
/* Modal version */
.avatar-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.avatar-modal .avatar-builder {
max-height: 90vh;
overflow-y: auto;
}
@media (max-width: 480px) {
.avatar-builder {
padding: 1rem;
}
.category-tabs {
gap: 0.35rem;
}
.category-tab {
padding: 0.4rem 0.5rem;
min-width: 50px;
}
.tab-icon {
font-size: 1rem;
}
.tab-label {
font-size: 0.6rem;
}
.options-grid {
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 0.35rem;
}
.option-btn {
padding: 0.5rem 0.35rem;
font-size: 0.75rem;
}
.avatar-builder-actions {
flex-direction: column;
}
.avatar-builder-actions .btn {
width: 100%;
}
}
+412
View File
@@ -0,0 +1,412 @@
import { useState, useEffect } from 'react';
import './AvatarBuilder.css';
// Avatar customization options
const AVATAR_OPTIONS = {
skinTone: [
{ id: 'light', color: '#FFE0BD' },
{ id: 'fair', color: '#FFCD94' },
{ id: 'medium', color: '#E5A073' },
{ id: 'tan', color: '#C68642' },
{ id: 'brown', color: '#8D5524' },
{ id: 'dark', color: '#5C4033' }
],
hairStyle: [
{ id: 'short', label: 'Short' },
{ id: 'medium', label: 'Medium' },
{ id: 'long', label: 'Long' },
{ id: 'spiky', label: 'Spiky' },
{ id: 'curly', label: 'Curly' },
{ id: 'mohawk', label: 'Mohawk' },
{ id: 'ponytail', label: 'Ponytail' },
{ id: 'bald', label: 'None' }
],
hairColor: [
{ id: 'black', color: '#1a1a1a' },
{ id: 'brown', color: '#4a3728' },
{ id: 'blonde', color: '#e6c86e' },
{ id: 'red', color: '#b55239' },
{ id: 'blue', color: '#4a90d9' },
{ id: 'pink', color: '#e75480' },
{ id: 'purple', color: '#9b59b6' },
{ id: 'green', color: '#27ae60' }
],
eyes: [
{ id: 'normal', label: 'Normal' },
{ id: 'happy', label: 'Happy' },
{ id: 'cool', label: 'Cool' },
{ id: 'surprised', label: 'Surprised' },
{ id: 'wink', label: 'Wink' },
{ id: 'sleepy', label: 'Sleepy' }
],
eyeColor: [
{ id: 'brown', color: '#5D4037' },
{ id: 'blue', color: '#2196F3' },
{ id: 'green', color: '#4CAF50' },
{ id: 'gray', color: '#607D8B' },
{ id: 'purple', color: '#9C27B0' },
{ id: 'gold', color: '#FFC107' }
],
mouth: [
{ id: 'smile', label: 'Smile' },
{ id: 'grin', label: 'Grin' },
{ id: 'open', label: 'Open' },
{ id: 'smirk', label: 'Smirk' },
{ id: 'neutral', label: 'Neutral' },
{ id: 'tongue', label: 'Tongue' }
],
accessory: [
{ id: 'none', label: 'None' },
{ id: 'glasses', label: 'Glasses' },
{ id: 'sunglasses', label: 'Sunglasses' },
{ id: 'headphones', label: 'Headphones' },
{ id: 'cap', label: 'Cap' },
{ id: 'crown', label: 'Crown' },
{ id: 'bow', label: 'Bow' },
{ id: 'horns', label: 'Horns' }
],
background: [
{ id: 'blue', color: '#3498db' },
{ id: 'green', color: '#2ecc71' },
{ id: 'purple', color: '#9b59b6' },
{ id: 'orange', color: '#e67e22' },
{ id: 'pink', color: '#e91e63' },
{ id: 'teal', color: '#1abc9c' },
{ id: 'red', color: '#e74c3c' },
{ id: 'gray', color: '#95a5a6' }
]
};
const DEFAULT_AVATAR = {
skinTone: 'medium',
hairStyle: 'short',
hairColor: 'brown',
eyes: 'normal',
eyeColor: 'brown',
mouth: 'smile',
accessory: 'none',
background: 'blue'
};
export default function AvatarBuilder({ initialData, onSave, onCancel }) {
const [avatar, setAvatar] = useState(() => ({
...DEFAULT_AVATAR,
...initialData
}));
const [activeCategory, setActiveCategory] = useState('skinTone');
const [saving, setSaving] = useState(false);
const handleChange = (category, value) => {
setAvatar(prev => ({ ...prev, [category]: value }));
};
const handleRandomize = () => {
const randomAvatar = {};
Object.keys(AVATAR_OPTIONS).forEach(category => {
const options = AVATAR_OPTIONS[category];
const randomIndex = Math.floor(Math.random() * options.length);
randomAvatar[category] = options[randomIndex].id;
});
setAvatar(randomAvatar);
};
const handleSave = async () => {
setSaving(true);
try {
await onSave(avatar);
} finally {
setSaving(false);
}
};
const categories = [
{ id: 'skinTone', label: 'Skin', icon: '👤' },
{ id: 'hairStyle', label: 'Hair', icon: '💇' },
{ id: 'hairColor', label: 'Hair Color', icon: '🎨' },
{ id: 'eyes', label: 'Eyes', icon: '👀' },
{ id: 'eyeColor', label: 'Eye Color', icon: '🔵' },
{ id: 'mouth', label: 'Mouth', icon: '👄' },
{ id: 'accessory', label: 'Accessory', icon: '🎀' },
{ id: 'background', label: 'Background', icon: '🖼️' }
];
return (
<div className="avatar-builder">
<div className="avatar-builder-preview">
<AvatarDisplay data={avatar} size={160} />
<button className="btn btn-secondary btn-sm randomize-btn" onClick={handleRandomize}>
🎲 Randomize
</button>
</div>
<div className="avatar-builder-controls">
<div className="category-tabs">
{categories.map(cat => (
<button
key={cat.id}
className={`category-tab ${activeCategory === cat.id ? 'active' : ''}`}
onClick={() => setActiveCategory(cat.id)}
title={cat.label}
>
<span className="tab-icon">{cat.icon}</span>
<span className="tab-label">{cat.label}</span>
</button>
))}
</div>
<div className="options-grid">
{AVATAR_OPTIONS[activeCategory].map(option => {
const isSelected = avatar[activeCategory] === option.id;
const isColor = option.color;
return (
<button
key={option.id}
className={`option-btn ${isSelected ? 'selected' : ''} ${isColor ? 'color-option' : ''}`}
onClick={() => handleChange(activeCategory, option.id)}
style={isColor ? { backgroundColor: option.color } : {}}
>
{!isColor && <span>{option.label}</span>}
{isSelected && <span className="check"></span>}
</button>
);
})}
</div>
</div>
<div className="avatar-builder-actions">
{onCancel && (
<button className="btn btn-secondary" onClick={onCancel}>
Cancel
</button>
)}
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
{saving ? 'Saving...' : 'Save Avatar'}
</button>
</div>
</div>
);
}
// Avatar Display Component
export function AvatarDisplay({ data, size = 48, className = '' }) {
const avatar = { ...DEFAULT_AVATAR, ...data };
const skinTone = AVATAR_OPTIONS.skinTone.find(s => s.id === avatar.skinTone)?.color || '#FFCD94';
const hairColor = AVATAR_OPTIONS.hairColor.find(h => h.id === avatar.hairColor)?.color || '#4a3728';
const eyeColor = AVATAR_OPTIONS.eyeColor.find(e => e.id === avatar.eyeColor)?.color || '#5D4037';
const bgColor = AVATAR_OPTIONS.background.find(b => b.id === avatar.background)?.color || '#3498db';
return (
<svg
width={size}
height={size}
viewBox="0 0 100 100"
className={`avatar-svg ${className}`}
style={{ backgroundColor: bgColor, borderRadius: '50%' }}
>
{/* Face */}
<ellipse cx="50" cy="55" rx="30" ry="35" fill={skinTone} />
{/* Hair - Back layer for some styles */}
{avatar.hairStyle === 'long' && (
<ellipse cx="50" cy="65" rx="35" ry="40" fill={hairColor} />
)}
{avatar.hairStyle === 'ponytail' && (
<>
<ellipse cx="50" cy="35" rx="32" ry="25" fill={hairColor} />
<ellipse cx="75" cy="70" rx="12" ry="20" fill={hairColor} />
</>
)}
{/* Face again (on top of back hair) */}
{(avatar.hairStyle === 'long' || avatar.hairStyle === 'ponytail') && (
<ellipse cx="50" cy="55" rx="30" ry="35" fill={skinTone} />
)}
{/* Eyes */}
<g className="eyes">
{avatar.eyes === 'normal' && (
<>
<ellipse cx="38" cy="50" rx="6" ry="7" fill="white" />
<ellipse cx="62" cy="50" rx="6" ry="7" fill="white" />
<circle cx="38" cy="51" r="4" fill={eyeColor} />
<circle cx="62" cy="51" r="4" fill={eyeColor} />
<circle cx="39" cy="49" r="1.5" fill="white" />
<circle cx="63" cy="49" r="1.5" fill="white" />
</>
)}
{avatar.eyes === 'happy' && (
<>
<path d="M32 50 Q38 45 44 50" stroke={eyeColor} strokeWidth="3" fill="none" strokeLinecap="round" />
<path d="M56 50 Q62 45 68 50" stroke={eyeColor} strokeWidth="3" fill="none" strokeLinecap="round" />
</>
)}
{avatar.eyes === 'cool' && (
<>
<path d="M32 48 L44 48 L44 54 L32 54 Z" fill={eyeColor} />
<path d="M56 48 L68 48 L68 54 L56 54 Z" fill={eyeColor} />
<rect x="33" y="49" width="4" height="4" fill="white" />
<rect x="57" y="49" width="4" height="4" fill="white" />
</>
)}
{avatar.eyes === 'surprised' && (
<>
<circle cx="38" cy="50" r="8" fill="white" />
<circle cx="62" cy="50" r="8" fill="white" />
<circle cx="38" cy="50" r="5" fill={eyeColor} />
<circle cx="62" cy="50" r="5" fill={eyeColor} />
<circle cx="39" cy="48" r="2" fill="white" />
<circle cx="63" cy="48" r="2" fill="white" />
</>
)}
{avatar.eyes === 'wink' && (
<>
<ellipse cx="38" cy="50" rx="6" ry="7" fill="white" />
<circle cx="38" cy="51" r="4" fill={eyeColor} />
<circle cx="39" cy="49" r="1.5" fill="white" />
<path d="M56 50 Q62 45 68 50" stroke={eyeColor} strokeWidth="3" fill="none" strokeLinecap="round" />
</>
)}
{avatar.eyes === 'sleepy' && (
<>
<path d="M32 52 Q38 48 44 52" stroke={eyeColor} strokeWidth="3" fill="none" strokeLinecap="round" />
<path d="M56 52 Q62 48 68 52" stroke={eyeColor} strokeWidth="3" fill="none" strokeLinecap="round" />
</>
)}
</g>
{/* Mouth */}
<g className="mouth">
{avatar.mouth === 'smile' && (
<path d="M38 68 Q50 78 62 68" stroke="#c0392b" strokeWidth="3" fill="none" strokeLinecap="round" />
)}
{avatar.mouth === 'grin' && (
<>
<path d="M35 65 Q50 80 65 65" fill="white" stroke="#c0392b" strokeWidth="2" />
<path d="M38 65 L62 65" stroke="#c0392b" strokeWidth="1" />
</>
)}
{avatar.mouth === 'open' && (
<ellipse cx="50" cy="70" rx="8" ry="6" fill="#c0392b" />
)}
{avatar.mouth === 'smirk' && (
<path d="M42 68 Q55 72 62 65" stroke="#c0392b" strokeWidth="3" fill="none" strokeLinecap="round" />
)}
{avatar.mouth === 'neutral' && (
<line x1="40" y1="68" x2="60" y2="68" stroke="#c0392b" strokeWidth="3" strokeLinecap="round" />
)}
{avatar.mouth === 'tongue' && (
<>
<path d="M38 68 Q50 78 62 68" stroke="#c0392b" strokeWidth="3" fill="none" strokeLinecap="round" />
<ellipse cx="50" cy="74" rx="5" ry="4" fill="#e74c3c" />
</>
)}
</g>
{/* Hair - Front layer */}
{avatar.hairStyle === 'short' && (
<path d="M20 45 Q20 20 50 18 Q80 20 80 45 L75 40 Q50 25 25 40 Z" fill={hairColor} />
)}
{avatar.hairStyle === 'medium' && (
<path d="M18 55 Q15 20 50 15 Q85 20 82 55 L78 45 Q50 22 22 45 Z" fill={hairColor} />
)}
{avatar.hairStyle === 'long' && (
<path d="M18 55 Q15 20 50 15 Q85 20 82 55 L78 45 Q50 22 22 45 Z" fill={hairColor} />
)}
{avatar.hairStyle === 'spiky' && (
<>
<path d="M25 35 L30 10 L40 30 L50 5 L60 30 L70 10 L75 35 Q50 25 25 35" fill={hairColor} />
</>
)}
{avatar.hairStyle === 'curly' && (
<>
<circle cx="25" cy="30" r="12" fill={hairColor} />
<circle cx="40" cy="22" r="12" fill={hairColor} />
<circle cx="60" cy="22" r="12" fill={hairColor} />
<circle cx="75" cy="30" r="12" fill={hairColor} />
<circle cx="50" cy="18" r="10" fill={hairColor} />
</>
)}
{avatar.hairStyle === 'mohawk' && (
<path d="M45 40 L45 5 Q50 0 55 5 L55 40 Q50 35 45 40" fill={hairColor} />
)}
{avatar.hairStyle === 'ponytail' && (
<path d="M20 45 Q20 20 50 18 Q80 20 80 45 L75 40 Q50 25 25 40 Z" fill={hairColor} />
)}
{/* Accessories */}
{avatar.accessory === 'glasses' && (
<>
<circle cx="38" cy="50" r="10" fill="none" stroke="#333" strokeWidth="2" />
<circle cx="62" cy="50" r="10" fill="none" stroke="#333" strokeWidth="2" />
<line x1="48" y1="50" x2="52" y2="50" stroke="#333" strokeWidth="2" />
<line x1="28" y1="48" x2="20" y2="45" stroke="#333" strokeWidth="2" />
<line x1="72" y1="48" x2="80" y2="45" stroke="#333" strokeWidth="2" />
</>
)}
{avatar.accessory === 'sunglasses' && (
<>
<rect x="28" y="44" width="18" height="12" rx="2" fill="#1a1a1a" />
<rect x="54" y="44" width="18" height="12" rx="2" fill="#1a1a1a" />
<line x1="46" y1="50" x2="54" y2="50" stroke="#1a1a1a" strokeWidth="3" />
<line x1="28" y1="48" x2="20" y2="45" stroke="#1a1a1a" strokeWidth="2" />
<line x1="72" y1="48" x2="80" y2="45" stroke="#1a1a1a" strokeWidth="2" />
</>
)}
{avatar.accessory === 'headphones' && (
<>
<path d="M20 50 Q20 25 50 22 Q80 25 80 50" stroke="#333" strokeWidth="4" fill="none" />
<rect x="14" y="45" width="10" height="18" rx="3" fill="#333" />
<rect x="76" y="45" width="10" height="18" rx="3" fill="#333" />
</>
)}
{avatar.accessory === 'cap' && (
<>
<path d="M20 40 Q20 20 50 18 Q80 20 80 40 L85 42 L15 42 Z" fill="#e74c3c" />
<rect x="15" y="38" width="70" height="6" fill="#c0392b" />
<rect x="75" y="35" width="15" height="8" rx="2" fill="#e74c3c" />
</>
)}
{avatar.accessory === 'crown' && (
<>
<path d="M25 30 L30 15 L40 25 L50 10 L60 25 L70 15 L75 30 Z" fill="#f1c40f" stroke="#d4a500" strokeWidth="1" />
<circle cx="30" cy="17" r="3" fill="#e74c3c" />
<circle cx="50" cy="12" r="3" fill="#3498db" />
<circle cx="70" cy="17" r="3" fill="#2ecc71" />
</>
)}
{avatar.accessory === 'bow' && (
<>
<ellipse cx="25" cy="25" rx="10" ry="8" fill="#e91e63" />
<ellipse cx="35" cy="25" rx="10" ry="8" fill="#e91e63" />
<circle cx="30" cy="25" r="5" fill="#c2185b" />
</>
)}
{avatar.accessory === 'horns' && (
<>
<path d="M25 35 Q15 20 28 10" stroke="#8b4513" strokeWidth="6" fill="none" strokeLinecap="round" />
<path d="M75 35 Q85 20 72 10" stroke="#8b4513" strokeWidth="6" fill="none" strokeLinecap="round" />
</>
)}
</svg>
);
}
// Mini avatar for lists/headers
export function AvatarMini({ data, size = 32, className = '' }) {
if (!data || Object.keys(data).length === 0) {
return (
<div
className={`avatar-mini-placeholder ${className}`}
style={{ width: size, height: size, fontSize: size * 0.6 }}
>
👤
</div>
);
}
return <AvatarDisplay data={data} size={size} className={className} />;
}
export { AVATAR_OPTIONS, DEFAULT_AVATAR };
+141
View File
@@ -0,0 +1,141 @@
/* Bug Report Modal Overlay */
.bug-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
/* Bug Report Modal Container */
.bug-modal.bug-report-modal {
background: white;
border-radius: 1rem;
padding: 1.5rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
position: relative;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}
/* Close button */
.bug-modal-close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
color: #94a3b8;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
}
.bug-modal-close:hover {
background: #f1f5f9;
color: #475569;
}
.bug-report-modal h2 {
margin: 0 0 0.5rem;
color: #1e293b;
}
.bug-report-modal .modal-subtitle {
color: #64748b;
margin: 0 0 1.5rem;
font-size: 0.9rem;
}
.bug-report-modal .form-group {
margin-bottom: 1rem;
}
.bug-report-modal label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: #374151;
}
.bug-report-modal input,
.bug-report-modal textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s;
}
.bug-report-modal input:focus,
.bug-report-modal textarea:focus {
outline: none;
border-color: #6366f1;
}
.bug-report-modal textarea {
resize: vertical;
min-height: 100px;
}
.bug-report-modal .page-info {
font-size: 0.8rem;
color: #64748b;
margin: 0.5rem 0 1rem;
}
.bug-report-modal .page-info code {
background: #f1f5f9;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.bug-report-modal .modal-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
.bug-report-modal .error-alert {
background: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 0.75rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.bug-success {
text-align: center;
padding: 1rem 0;
}
.bug-success .success-icon {
font-size: 3rem;
display: block;
margin-bottom: 1rem;
}
.bug-success h2 {
margin: 0 0 0.5rem;
}
.bug-success p {
color: #64748b;
margin: 0 0 1.5rem;
}
@@ -0,0 +1,96 @@
import { useState } from 'react';
import api from '../api';
import './BugReportModal.css';
export default function BugReportModal({ onClose }) {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setSubmitting(true);
try {
await api.submitBugReport(title, description, window.location.pathname);
setSubmitted(true);
} catch (err) {
setError(err.message || 'Failed to submit bug report');
} finally {
setSubmitting(false);
}
};
if (submitted) {
return (
<div className="bug-modal-overlay" onClick={onClose}>
<div className="bug-modal bug-report-modal" onClick={e => e.stopPropagation()}>
<div className="bug-success">
<span className="success-icon"></span>
<h2>Thank You!</h2>
<p>Your bug report has been submitted. We'll notify you when it's fixed.</p>
<button onClick={onClose} className="btn btn-primary">Close</button>
</div>
</div>
</div>
);
}
return (
<div className="bug-modal-overlay" onClick={onClose}>
<div className="bug-modal bug-report-modal" onClick={e => e.stopPropagation()}>
<button className="bug-modal-close" onClick={onClose}>&times;</button>
<h2>Report a Bug</h2>
<p className="modal-subtitle">Help us improve GamerComp by reporting issues you find.</p>
{error && <div className="error-alert">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="bug-title">What's the issue? (brief title)</label>
<input
type="text"
id="bug-title"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="e.g., Game won't load on mobile"
required
minLength={5}
maxLength={200}
/>
</div>
<div className="form-group">
<label htmlFor="bug-description">Describe the bug</label>
<textarea
id="bug-description"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="What happened? What did you expect to happen? Any steps to reproduce?"
required
minLength={10}
maxLength={5000}
rows={5}
/>
</div>
<p className="page-info">
Page: <code>{window.location.pathname}</code>
</p>
<div className="modal-actions">
<button type="button" onClick={onClose} className="btn btn-secondary" disabled={submitting}>
Cancel
</button>
<button type="submit" className="btn btn-primary" disabled={submitting}>
{submitting ? 'Submitting...' : 'Submit Bug Report'}
</button>
</div>
</form>
</div>
</div>
);
}
+659
View File
@@ -0,0 +1,659 @@
/* Floating Action Button */
.dev-toolbar-fab {
position: fixed;
bottom: 80px;
right: 16px;
z-index: 9999;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
border: none;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.dev-toolbar-fab:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.5);
}
.dev-toolbar-fab.expanded {
background: #64748b;
}
/* Panel */
.dev-toolbar-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
max-height: 70vh;
z-index: 9998;
background: white;
border-radius: 1rem 1rem 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Header */
.dev-toolbar-header {
padding: 12px 16px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 1rem 1rem 0 0;
}
.dev-toolbar-title {
display: flex;
align-items: center;
gap: 8px;
}
.dev-toolbar-title strong {
font-size: 1rem;
}
.status-pill {
padding: 2px 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
font-size: 0.75rem;
text-transform: capitalize;
}
.status-pill.status-testing {
background: rgba(251, 191, 36, 0.3);
}
.status-pill.status-draft {
background: rgba(148, 163, 184, 0.3);
}
.dev-toolbar-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.dev-toolbar-close:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Tabs */
.dev-toolbar-tabs {
display: flex;
border-bottom: 1px solid #e2e8f0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.dev-tab {
flex: 1;
min-width: 80px;
padding: 10px 12px;
border: none;
background: white;
color: #64748b;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.dev-tab:hover {
background: #f8fafc;
}
.dev-tab.active {
color: #6366f1;
background: #f0edff;
border-bottom: 2px solid #6366f1;
}
/* Content */
.dev-toolbar-content {
padding: 16px;
overflow-y: auto;
flex: 1;
}
.dev-tab-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.dev-label {
font-size: 0.85rem;
font-weight: 600;
color: #374151;
}
.dev-hint {
font-size: 0.8rem;
color: #64748b;
margin: 0;
}
.dev-input-row {
display: flex;
gap: 8px;
}
.dev-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.95rem;
}
.dev-input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.dev-textarea {
width: 100%;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.95rem;
font-family: inherit;
resize: vertical;
min-height: 80px;
}
.dev-textarea:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.dev-prompt {
background: #f8fafc;
padding: 12px;
border-radius: 8px;
font-size: 0.8rem;
font-family: monospace;
white-space: pre-wrap;
word-break: break-word;
max-height: 40vh;
overflow-y: auto;
color: #475569;
border: 1px solid #e2e8f0;
-webkit-overflow-scrolling: touch;
}
/* Mobile: ensure prompt tab content is visible */
@media (max-width: 768px) {
.dev-prompt {
max-height: 35vh;
font-size: 0.75rem;
}
}
.dev-actions {
display: flex;
flex-direction: column;
gap: 10px;
}
.dev-actions .btn {
width: 100%;
padding: 12px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.dev-actions .btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
border: none;
}
.dev-actions .btn-primary:hover:not(:disabled) {
opacity: 0.9;
}
.dev-actions .btn-secondary {
background: #f1f5f9;
color: #475569;
border: 1px solid #e2e8f0;
}
.dev-actions .btn-secondary:hover {
background: #e2e8f0;
}
.dev-actions .btn-danger {
background: white;
color: #dc2626;
border: 1px solid #fecaca;
}
.dev-actions .btn-danger:hover {
background: #fef2f2;
}
.dev-actions .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.dev-error {
background: #fef2f2;
color: #dc2626;
padding: 10px 12px;
border-radius: 8px;
font-size: 0.85rem;
border: 1px solid #fecaca;
}
.dev-success {
background: #f0fdf4;
color: #16a34a;
padding: 10px 12px;
border-radius: 8px;
font-size: 0.85rem;
border: 1px solid #bbf7d0;
}
/* Desktop styles */
@media (min-width: 768px) {
.dev-toolbar-fab {
bottom: 24px;
right: 24px;
}
.dev-toolbar-panel {
left: auto;
right: 24px;
bottom: 90px;
width: 380px;
max-height: 500px;
border-radius: 1rem;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.dev-toolbar-header {
border-radius: 1rem 1rem 0 0;
}
.dev-actions {
flex-direction: row;
flex-wrap: wrap;
}
.dev-actions .btn {
flex: 1;
min-width: calc(50% - 5px);
}
.dev-actions .btn:last-child {
flex: none;
width: 100%;
}
}
/* Testing Timer */
.dev-new-game-banner {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 10px 14px;
border-radius: 8px;
font-weight: 500;
text-align: center;
margin-bottom: 12px;
}
.dev-testing-timer {
background: #f8fafc;
border-radius: 10px;
padding: 14px;
margin-bottom: 12px;
border: 1px solid #e2e8f0;
}
.timer-display {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.timer-icon {
font-size: 1.2rem;
}
.timer-value {
font-size: 1.5rem;
font-weight: 700;
font-family: 'Monaco', 'Menlo', monospace;
color: #6366f1;
}
.timer-label {
font-size: 0.85rem;
color: #64748b;
}
.timer-progress {
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.timer-progress-bar {
height: 100%;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-radius: 4px;
transition: width 1s linear;
}
.btn-disabled {
background: #94a3b8;
color: white;
cursor: not-allowed;
opacity: 0.7;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.85rem;
}
/* Code Editor Section */
.dev-code-section {
margin-top: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid #e2e8f0;
}
.dev-code-btn {
width: 100%;
margin-top: 0.25rem;
}
/* Code Editor Modal */
.code-editor-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 1rem;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.code-editor-modal {
background: #1e293b;
border-radius: 12px;
width: 100%;
max-width: 900px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
animation: slideUp 0.25s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.code-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-radius: 12px 12px 0 0;
}
.code-editor-header h3 {
margin: 0;
color: white;
font-size: 1.1rem;
}
.code-editor-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.code-editor-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.code-editor-body {
flex: 1;
overflow: hidden;
display: flex;
}
.code-editor-textarea {
width: 100%;
flex: 1;
padding: 1rem;
background: #0f172a;
color: #e2e8f0;
border: none;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 0.85rem;
line-height: 1.5;
resize: none;
min-height: 400px;
}
.code-editor-textarea:focus {
outline: none;
}
.code-editor-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: #1e293b;
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0 0 12px 12px;
}
.code-editor-info {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: #64748b;
}
.code-modified {
color: #fbbf24;
font-weight: 500;
}
.code-editor-buttons {
display: flex;
gap: 0.5rem;
}
.code-editor-buttons .btn {
padding: 0.5rem 1rem;
}
/* Code Confirm Modal */
.code-confirm-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
padding: 1rem;
}
.code-confirm-modal {
background: linear-gradient(145deg, #1e293b, #0f172a);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 16px;
padding: 1.5rem;
max-width: 400px;
width: 100%;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.code-confirm-modal h3 {
margin: 0 0 1rem 0;
color: #fff;
}
.code-confirm-summary {
color: #94a3b8;
line-height: 1.5;
margin: 0 0 1rem 0;
}
.code-confirm-stats {
display: flex;
justify-content: center;
gap: 1.5rem;
font-size: 0.85rem;
color: #64748b;
margin-bottom: 1.25rem;
}
.code-confirm-buttons {
display: flex;
gap: 0.75rem;
}
.code-confirm-buttons .btn {
flex: 1;
}
/* Mobile adjustments for code editor */
@media (max-width: 768px) {
.code-editor-modal {
max-height: 95vh;
border-radius: 8px;
}
.code-editor-header {
border-radius: 8px 8px 0 0;
}
.code-editor-textarea {
font-size: 0.8rem;
min-height: 300px;
}
.code-editor-footer {
flex-direction: column;
gap: 0.75rem;
border-radius: 0 0 8px 8px;
}
.code-editor-buttons {
width: 100%;
}
.code-editor-buttons .btn {
flex: 1;
}
}
/* Ultra-small screens */
@media (max-width: 360px) {
.dev-toolbar-fab {
width: 48px;
height: 48px;
font-size: 20px;
bottom: 70px;
right: 10px;
}
.dev-toolbar-panel {
max-height: 75vh;
}
.dev-tab {
padding: 8px 10px;
font-size: 0.8rem;
min-width: 70px;
}
.dev-toolbar-content {
padding: 12px;
}
.code-confirm-buttons {
flex-direction: column;
}
}
+553
View File
@@ -0,0 +1,553 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import api from '../api';
import TweakPanel from './TweakPanel';
import './DevToolbar.css';
// Floating Developer Toolbar for game creators to test/edit their games
export default function DevToolbar({ game, user, onGameUpdate }) {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const isNewGame = searchParams.get('newGame') === 'true';
const [isExpanded, setIsExpanded] = useState(isNewGame);
const [activeTab, setActiveTab] = useState(isNewGame ? 'testing' : 'actions');
const [title, setTitle] = useState(game?.title || '');
const [feedback, setFeedback] = useState('');
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
// Code editor state
const [showCodeEditor, setShowCodeEditor] = useState(false);
const [editedCode, setEditedCode] = useState('');
const [codeReviewing, setCodeReviewing] = useState(false);
const [codeSaving, setCodeSaving] = useState(false);
const [codeReviewResult, setCodeReviewResult] = useState(null);
const [showCodeConfirm, setShowCodeConfirm] = useState(false);
// Testing state
const [testingSeconds, setTestingSeconds] = useState(game?.creatorPlayDuration || 0);
const [isTestingActive, setIsTestingActive] = useState(false);
const testingIntervalRef = useRef(null);
const REQUIRED_TEST_TIME = 60; // 1 minute
// Don't render if not the creator or game is published
if (!user || !game) return null;
if (user.id !== game.creatorId) return null;
if (game.status === 'published' || game.status === 'pending') return null;
const canPublish = testingSeconds >= REQUIRED_TEST_TIME || game.creatorPlayed;
// Start testing timer when component mounts for new games
useEffect(() => {
if (isNewGame && !isTestingActive) {
startTesting();
}
return () => {
if (testingIntervalRef.current) {
clearInterval(testingIntervalRef.current);
}
};
}, [isNewGame]);
const startTesting = () => {
if (isTestingActive) return;
setIsTestingActive(true);
testingIntervalRef.current = setInterval(() => {
setTestingSeconds(prev => prev + 1);
}, 1000);
};
const stopTesting = () => {
setIsTestingActive(false);
if (testingIntervalRef.current) {
clearInterval(testingIntervalRef.current);
testingIntervalRef.current = null;
}
};
// Save testing progress periodically
useEffect(() => {
if (testingSeconds > 0 && testingSeconds % 10 === 0) {
// Save every 10 seconds
api.trackTesting(game.id, testingSeconds, 0).catch(() => {});
}
}, [testingSeconds, game.id]);
const handleSaveTitle = async () => {
if (!title.trim()) return;
setSaving(true);
setError('');
try {
await api.updateGame(game.id, { title });
setSuccess('Title saved!');
setTimeout(() => setSuccess(''), 2000);
if (onGameUpdate) onGameUpdate({ ...game, title });
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
};
// Code editor handlers
const handleOpenCodeEditor = () => {
setEditedCode(game?.game_code || '');
setShowCodeEditor(true);
setCodeReviewResult(null);
};
const handleCloseCodeEditor = () => {
setShowCodeEditor(false);
setEditedCode('');
setCodeReviewResult(null);
setShowCodeConfirm(false);
};
const handleReviewCode = async () => {
if (!editedCode.trim() || editedCode === game?.game_code) {
setError('No changes detected');
return;
}
setCodeReviewing(true);
setError('');
try {
const result = await api.reviewCodeChanges(game.id, editedCode);
setCodeReviewResult(result);
setShowCodeConfirm(true);
} catch (err) {
setError(err.message);
} finally {
setCodeReviewing(false);
}
};
const handleSaveCode = async () => {
setCodeSaving(true);
setError('');
try {
await api.saveGameCode(game.id, editedCode);
setShowCodeEditor(false);
setShowCodeConfirm(false);
setSuccess('Code saved! Reloading...');
setTimeout(() => window.location.reload(), 1500);
} catch (err) {
setError(err.message);
} finally {
setCodeSaving(false);
}
};
const handleCancelCodeConfirm = () => {
setShowCodeConfirm(false);
setCodeReviewResult(null);
};
const [refineProgress, setRefineProgress] = useState('');
const [refinePercent, setRefinePercent] = useState(0);
const handleRefine = async () => {
if (!feedback.trim()) {
setError('Please describe what you want to change');
return;
}
setSaving(true);
setError('');
setRefineProgress('Starting AI update...');
setRefinePercent(0);
stopTesting();
try {
// This now returns immediately
await api.refineGame(game.id, feedback);
// Open SSE for progress
const es = api.streamGeneration(game.id, (event) => {
if (event.type === 'phase') {
setRefineProgress(event.message);
setRefinePercent(event.progress || 0);
} else if (event.type === 'progress') {
setRefinePercent(event.progress || 0);
} else if (event.type === 'complete') {
setRefineProgress('');
setRefinePercent(0);
setSaving(false);
setSuccess(event.summary ? `Done! ${event.summary}` : 'Game updated! Reloading...');
setFeedback('');
es.close();
setTimeout(() => window.location.reload(), 1500);
} else if (event.type === 'error') {
setRefineProgress('');
setRefinePercent(0);
setSaving(false);
setError(event.error || 'Update failed. Try again.');
es.close();
}
});
} catch (err) {
setSaving(false);
setRefineProgress('');
setError(err.message);
}
};
const handlePublish = async () => {
if (!canPublish) {
setError(`Please test your game for at least ${REQUIRED_TEST_TIME - testingSeconds} more seconds.`);
return;
}
stopTesting();
// Save final testing time
await api.trackTesting(game.id, testingSeconds, 0).catch(() => {});
navigate(`/publish/${game.id}`);
};
const handleSubmit = async () => {
if (!canPublish) {
setError(`Please test your game for at least ${REQUIRED_TEST_TIME - testingSeconds} more seconds.`);
return;
}
if (!confirm('Submit this game for review?')) return;
setSaving(true);
stopTesting();
try {
await api.trackTesting(game.id, testingSeconds, 0);
await api.submitGame(game.id);
setSuccess('Submitted for review!');
setTimeout(() => navigate('/dashboard'), 1500);
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!confirm('Are you sure you want to delete this game? This cannot be undone.')) return;
setSaving(true);
stopTesting();
try {
await api.deleteGame(game.id);
navigate('/dashboard');
} catch (err) {
setError(err.message);
setSaving(false);
}
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${String(secs).padStart(2, '0')}`;
};
const tabs = [
{ id: 'testing', label: 'Testing' },
{ id: 'edit', label: 'Edit' },
{ id: 'tweaks', label: 'Free Tweaks' },
{ id: 'refine', label: 'AI Overhaul' },
{ id: 'prompt', label: 'Prompt' }
];
return (
<>
{/* Floating button - always visible */}
<button
className={`dev-toolbar-fab ${isExpanded ? 'expanded' : ''}`}
onClick={() => setIsExpanded(!isExpanded)}
aria-label={isExpanded ? 'Close dev tools' : 'Open dev tools'}
>
{isExpanded ? '✕' : '🛠️'}
</button>
{/* Expanded panel */}
{isExpanded && (
<div className="dev-toolbar-panel">
{/* Header */}
<div className="dev-toolbar-header">
<div className="dev-toolbar-title">
<strong>Dev Tools</strong>
<span className={`status-pill status-${game.status}`}>
{game.status}
</span>
</div>
<button
className="dev-toolbar-close"
onClick={() => setIsExpanded(false)}
>
</button>
</div>
{/* Tabs */}
<div className="dev-toolbar-tabs">
{tabs.map(tab => (
<button
key={tab.id}
className={`dev-tab ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
{/* Tab content */}
<div className="dev-toolbar-content">
{error && <div className="dev-error">{error}</div>}
{success && <div className="dev-success">{success}</div>}
{/* Testing Tab */}
{activeTab === 'testing' && (
<div className="dev-tab-content">
{isNewGame && (
<div className="dev-new-game-banner">
Your game is ready! Play it to make sure it works.
</div>
)}
<div className="dev-testing-timer">
<div className="timer-display">
<span className="timer-icon">{isTestingActive ? '⏱️' : '⏸️'}</span>
<span className="timer-value">{formatTime(testingSeconds)}</span>
<span className="timer-label">/ {formatTime(REQUIRED_TEST_TIME)} required</span>
</div>
<div className="timer-progress">
<div
className="timer-progress-bar"
style={{ width: `${Math.min(100, (testingSeconds / REQUIRED_TEST_TIME) * 100)}%` }}
/>
</div>
{!isTestingActive && testingSeconds < REQUIRED_TEST_TIME && (
<button className="btn btn-secondary btn-sm" onClick={startTesting}>
Resume Timer
</button>
)}
</div>
<p className="dev-hint">
{canPublish
? '✅ You can now publish your game!'
: `Play your game for ${REQUIRED_TEST_TIME - testingSeconds} more seconds to unlock publishing.`
}
</p>
<div className="dev-actions">
<button
className={`btn ${canPublish ? 'btn-primary' : 'btn-disabled'}`}
onClick={handlePublish}
disabled={!canPublish || saving}
>
{canPublish ? 'Publish Game! 🚀' : `Test ${REQUIRED_TEST_TIME - testingSeconds}s more`}
</button>
<button
className="btn btn-secondary"
onClick={() => navigate('/dashboard')}
>
Save & Exit
</button>
<button
className="btn btn-danger"
onClick={handleDelete}
disabled={saving}
>
Delete Game
</button>
</div>
</div>
)}
{/* Edit Tab */}
{activeTab === 'edit' && (
<div className="dev-tab-content">
<label className="dev-label">Game Title</label>
<div className="dev-input-row">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="dev-input"
maxLength={100}
/>
<button
className="btn btn-primary"
onClick={handleSaveTitle}
disabled={saving || !title.trim()}
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
<div className="dev-code-section">
<label className="dev-label">Game Code</label>
<button
className="btn btn-secondary dev-code-btn"
onClick={handleOpenCodeEditor}
>
View & Edit Code
</button>
<p className="dev-hint">
Advanced: Manually view and edit the game's HTML/JavaScript code.
</p>
</div>
</div>
)}
{/* Free Tweaks Tab */}
{activeTab === 'tweaks' && (
<div className="dev-tab-content">
<TweakPanel
game={game}
onTweakApplied={() => window.location.reload()}
onFallbackNeeded={(tweakFeedback) => {
setFeedback(tweakFeedback || '');
setActiveTab('refine');
}}
/>
</div>
)}
{/* Refine Tab */}
{activeTab === 'refine' && (
<div className="dev-tab-content">
<label className="dev-label">Describe what you want to change</label>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
className="dev-textarea"
placeholder="e.g., 'Make the player faster' or 'Add more enemies' or 'Change colors to blue'"
rows={4}
disabled={saving}
/>
{refineProgress && (
<div className="dev-refine-progress">
<p style={{ fontSize: '0.85rem', margin: '0.5rem 0' }}>{refineProgress}</p>
<div style={{ width: '100%', height: '6px', background: 'rgba(255,255,255,0.1)', borderRadius: '3px', overflow: 'hidden' }}>
<div style={{
width: `${refinePercent}%`, height: '100%',
background: 'linear-gradient(90deg, #6366f1, #8b5cf6)',
borderRadius: '3px', transition: 'width 0.5s ease'
}} />
</div>
</div>
)}
<button
className="btn btn-primary"
onClick={handleRefine}
disabled={saving || !feedback.trim()}
>
{saving ? `Updating... ${refinePercent}%` : 'Update with AI'}
</button>
<p className="dev-hint">
{saving
? 'You can close this page - you\'ll get a notification when done!'
: 'The AI will modify your game based on your feedback.'}
</p>
</div>
)}
{/* Prompt Tab */}
{activeTab === 'prompt' && (
<div className="dev-tab-content">
<label className="dev-label">
{game.promptWasEdited ? '✏️ Edited Prompt' : 'Original Prompt'}
</label>
{game.creationPrompt || game.prompt ? (
<pre className="dev-prompt">
{game.creationPrompt || game.prompt}
</pre>
) : (
<p className="dev-hint">No prompt data available for this game.</p>
)}
{game.promptWasEdited && game.originalPrompt && (
<>
<label className="dev-label" style={{ marginTop: '1rem' }}>
Original Auto-Generated Prompt
</label>
<pre className="dev-prompt">
{game.originalPrompt}
</pre>
</>
)}
</div>
)}
</div>
</div>
)}
{/* Code Editor Modal */}
{showCodeEditor && (
<div className="code-editor-overlay" onClick={handleCloseCodeEditor}>
<div className="code-editor-modal" onClick={e => e.stopPropagation()}>
<div className="code-editor-header">
<h3>Edit Game Code</h3>
<button className="code-editor-close" onClick={handleCloseCodeEditor}>
&times;
</button>
</div>
<div className="code-editor-body">
<textarea
className="code-editor-textarea"
value={editedCode}
onChange={(e) => setEditedCode(e.target.value)}
spellCheck={false}
placeholder="Game HTML/JavaScript code..."
/>
</div>
<div className="code-editor-footer">
<div className="code-editor-info">
<span>{editedCode.length.toLocaleString()} characters</span>
{editedCode !== game?.game_code && (
<span className="code-modified">Modified</span>
)}
</div>
<div className="code-editor-buttons">
<button className="btn btn-secondary" onClick={handleCloseCodeEditor}>
Cancel
</button>
<button
className="btn btn-primary"
onClick={handleReviewCode}
disabled={codeReviewing || editedCode === game?.game_code}
>
{codeReviewing ? 'Reviewing...' : 'Review & Save'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Code Review Confirmation Modal */}
{showCodeConfirm && codeReviewResult && (
<div className="code-confirm-overlay" onClick={handleCancelCodeConfirm}>
<div className="code-confirm-modal" onClick={e => e.stopPropagation()}>
<h3>Review Changes</h3>
<p className="code-confirm-summary">{codeReviewResult.summary}</p>
<div className="code-confirm-stats">
<span>Before: {codeReviewResult.oldCodeLength?.toLocaleString()} chars</span>
<span>After: {codeReviewResult.newCodeLength?.toLocaleString()} chars</span>
</div>
<div className="code-confirm-buttons">
<button className="btn btn-secondary" onClick={handleCancelCodeConfirm}>
Keep Editing
</button>
<button
className="btn btn-primary"
onClick={handleSaveCode}
disabled={codeSaving}
>
{codeSaving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
</div>
)}
</>
);
}
+173
View File
@@ -0,0 +1,173 @@
.tooltip-wrapper {
position: relative;
display: inline-block;
}
.feature-tooltip {
position: absolute;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
pointer-events: none;
}
.feature-tooltip.visible {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
/* Positioning */
.feature-tooltip.bottom {
top: calc(100% + 10px);
left: 50%;
transform: translateX(-50%) translateY(-5px);
}
.feature-tooltip.bottom.visible {
transform: translateX(-50%) translateY(0);
}
.feature-tooltip.top {
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%) translateY(5px);
}
.feature-tooltip.top.visible {
transform: translateX(-50%) translateY(0);
}
.feature-tooltip.left {
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%) translateX(5px);
}
.feature-tooltip.left.visible {
transform: translateY(-50%) translateX(0);
}
.feature-tooltip.right {
left: calc(100% + 10px);
top: 50%;
transform: translateY(-50%) translateX(-5px);
}
.feature-tooltip.right.visible {
transform: translateY(-50%) translateX(0);
}
/* Content */
.tooltip-content {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
padding: 0.875rem 1rem;
border-radius: 0.75rem;
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
min-width: 180px;
max-width: 280px;
text-align: center;
}
.tooltip-text {
margin: 0 0 0.75rem;
font-size: 0.9rem;
line-height: 1.4;
}
.tooltip-dismiss {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 0.4rem 1rem;
border-radius: 1rem;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.tooltip-dismiss:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Arrow */
.tooltip-arrow {
position: absolute;
width: 12px;
height: 12px;
background: #6366f1;
transform: rotate(45deg);
}
.feature-tooltip.bottom .tooltip-arrow {
top: -6px;
left: 50%;
margin-left: -6px;
}
.feature-tooltip.top .tooltip-arrow {
bottom: -6px;
left: 50%;
margin-left: -6px;
}
.feature-tooltip.left .tooltip-arrow {
right: -6px;
top: 50%;
margin-top: -6px;
}
.feature-tooltip.right .tooltip-arrow {
left: -6px;
top: 50%;
margin-top: -6px;
}
/* Pulsing highlight */
.pulsing-highlight-wrapper {
position: relative;
display: inline-block;
}
.pulsing-ring {
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px solid #6366f1;
border-radius: 0.75rem;
animation: pulse-ring 1.5s ease-out infinite;
pointer-events: none;
}
@keyframes pulse-ring {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* Mobile adjustments */
@media (max-width: 480px) {
.tooltip-content {
min-width: 150px;
max-width: 220px;
padding: 0.75rem;
}
.tooltip-text {
font-size: 0.85rem;
}
}
+165
View File
@@ -0,0 +1,165 @@
import { useState, useEffect, useRef } from 'react';
import './FeatureTooltip.css';
const TOOLTIP_STORAGE_KEY = 'gamercomp_tooltips_seen';
// Get which tooltips have been dismissed
const getSeenTooltips = () => {
try {
return JSON.parse(localStorage.getItem(TOOLTIP_STORAGE_KEY) || '[]');
} catch {
return [];
}
};
// Mark a tooltip as seen
const markTooltipSeen = (tooltipId) => {
const seen = getSeenTooltips();
if (!seen.includes(tooltipId)) {
seen.push(tooltipId);
localStorage.setItem(TOOLTIP_STORAGE_KEY, JSON.stringify(seen));
}
};
// Check if tooltip has been seen
const hasSeenTooltip = (tooltipId) => {
return getSeenTooltips().includes(tooltipId);
};
export default function FeatureTooltip({
id,
children,
position = 'bottom',
delay = 500,
showOnce = true,
forceShow = false
}) {
const [visible, setVisible] = useState(false);
const [dismissed, setDismissed] = useState(false);
const timeoutRef = useRef(null);
useEffect(() => {
// Check if already seen
if (showOnce && hasSeenTooltip(id)) {
setDismissed(true);
return;
}
// Show tooltip after delay
if (forceShow || !dismissed) {
timeoutRef.current = setTimeout(() => {
setVisible(true);
}, delay);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [id, delay, showOnce, forceShow, dismissed]);
const handleDismiss = () => {
setVisible(false);
setDismissed(true);
if (showOnce) {
markTooltipSeen(id);
}
};
if (dismissed && !forceShow) {
return null;
}
return (
<div className={`feature-tooltip ${position} ${visible ? 'visible' : ''}`}>
<div className="tooltip-content">
<p className="tooltip-text">{children}</p>
<button className="tooltip-dismiss" onClick={handleDismiss}>
Got it!
</button>
</div>
<div className="tooltip-arrow" />
</div>
);
}
// Wrapper component that shows tooltip attached to a target
export function TooltipWrapper({
tooltipId,
tooltip,
position = 'bottom',
delay = 500,
showOnce = true,
children
}) {
const [visible, setVisible] = useState(false);
const [dismissed, setDismissed] = useState(false);
const timeoutRef = useRef(null);
useEffect(() => {
if (showOnce && hasSeenTooltip(tooltipId)) {
setDismissed(true);
return;
}
timeoutRef.current = setTimeout(() => {
setVisible(true);
}, delay);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [tooltipId, delay, showOnce]);
const handleDismiss = (e) => {
e.stopPropagation();
setVisible(false);
setDismissed(true);
if (showOnce) {
markTooltipSeen(tooltipId);
}
};
if (dismissed) {
return children;
}
return (
<div className="tooltip-wrapper">
{children}
{visible && (
<div className={`feature-tooltip ${position} visible`}>
<div className="tooltip-content">
<p className="tooltip-text">{tooltip}</p>
<button className="tooltip-dismiss" onClick={handleDismiss}>
Got it!
</button>
</div>
<div className="tooltip-arrow" />
</div>
)}
</div>
);
}
// Simple pulsing highlight for elements
export function PulsingHighlight({ active, children }) {
if (!active) {
return children;
}
return (
<div className="pulsing-highlight-wrapper">
{children}
<div className="pulsing-ring" />
</div>
);
}
// Utility to reset all seen tooltips (for testing)
export const resetSeenTooltips = () => {
localStorage.removeItem(TOOLTIP_STORAGE_KEY);
};
+118
View File
@@ -0,0 +1,118 @@
.site-footer {
background: #1e293b;
color: #94a3b8;
padding: 3rem 1rem 1.5rem;
margin-top: auto;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
}
.footer-brand {
text-align: center;
margin-bottom: 2rem;
}
.footer-logo {
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.footer-tagline {
margin: 0.5rem 0 0;
font-size: 0.9rem;
}
.footer-links {
display: flex;
justify-content: center;
gap: 4rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.footer-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.footer-section h4 {
color: white;
font-size: 0.9rem;
margin: 0 0 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.footer-section a {
color: #94a3b8;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.2s;
}
.footer-section a:hover {
color: white;
}
.footer-link-btn {
background: none;
border: none;
color: #94a3b8;
text-decoration: none;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
padding: 0;
text-align: left;
transition: color 0.2s;
}
.footer-link-btn:hover {
color: white;
}
.footer-install-btn {
color: #60a5fa;
}
.footer-install-btn:hover {
color: #93c5fd;
}
.footer-bottom {
text-align: center;
padding-top: 1.5rem;
border-top: 1px solid #334155;
}
.footer-copyright {
margin: 0 0 0.25rem;
font-size: 0.85rem;
}
.footer-coppa {
margin: 0;
font-size: 0.8rem;
color: #64748b;
}
@media (max-width: 640px) {
.site-footer {
padding: 2rem 1rem 1rem;
}
.footer-links {
flex-direction: column;
gap: 1.5rem;
text-align: center;
}
.footer-section {
align-items: center;
}
}
+97
View File
@@ -0,0 +1,97 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import BugReportModal from './BugReportModal';
import './Footer.css';
export default function Footer() {
const { isAuthenticated, isGuest } = useAuth();
const [showBugModal, setShowBugModal] = useState(false);
const [installPrompt, setInstallPrompt] = useState(null);
const [showInstallOption, setShowInstallOption] = useState(false);
const canReportBugs = isAuthenticated;
// PWA install prompt handler
useEffect(() => {
const handleBeforeInstall = (e) => {
e.preventDefault();
setInstallPrompt(e);
if (!window.matchMedia('(display-mode: standalone)').matches) {
setShowInstallOption(true);
}
};
window.addEventListener('beforeinstallprompt', handleBeforeInstall);
if (window.matchMedia('(display-mode: standalone)').matches) {
setShowInstallOption(false);
}
return () => window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
}, []);
const handleInstallApp = async () => {
if (!installPrompt) return;
installPrompt.prompt();
const { outcome } = await installPrompt.userChoice;
if (outcome === 'accepted') {
setShowInstallOption(false);
}
setInstallPrompt(null);
};
return (
<footer className="site-footer">
<div className="footer-content">
<div className="footer-brand">
<span className="footer-logo">🎮 GamerComp</span>
<p className="footer-tagline">Gamer Composer - Where everyone composes games with AI</p>
</div>
<div className="footer-links">
<div className="footer-section">
<h4>Play</h4>
<Link to="/arcade">Arcade</Link>
<Link to="/create">Create a Game</Link>
</div>
<div className="footer-section">
<h4>Account</h4>
<Link to="/login">Login</Link>
<Link to="/register">Sign Up</Link>
<Link to="/dashboard">Dashboard</Link>
</div>
<div className="footer-section">
<h4>Help</h4>
<Link to="/terms">Terms of Service</Link>
<Link to="/privacy">Privacy Policy</Link>
<Link to="/release-notes">Release Notes</Link>
{canReportBugs && (
<button className="footer-link-btn" onClick={() => setShowBugModal(true)}>
Report a Bug
</button>
)}
{showInstallOption && (
<button className="footer-link-btn footer-install-btn" onClick={handleInstallApp}>
📲 Install App
</button>
)}
</div>
</div>
<div className="footer-bottom">
<p className="footer-copyright">
&copy; {new Date().getFullYear()} GamerComp. All games are open source under GPL.
</p>
<p className="footer-coppa">
Designed for ages 8-16. COPPA compliant.
</p>
</div>
</div>
{showBugModal && <BugReportModal onClose={() => setShowBugModal(false)} />}
</footer>
);
}
+413
View File
@@ -0,0 +1,413 @@
.game-card {
background: #1e1b4b;
border-radius: 1rem;
overflow: hidden;
text-decoration: none;
color: inherit;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
display: block;
-webkit-tap-highlight-color: transparent;
aspect-ratio: 16 / 10;
}
.game-card-inner {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
/* Hover styles only for devices that support hover */
@media (hover: hover) {
.game-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.game-card:hover .play-overlay {
opacity: 1;
}
.game-card:hover .play-icon {
transform: scale(1.1);
}
}
/* Touch/active states for touch devices */
@media (hover: none) {
.game-card:active {
transform: scale(0.98);
}
.game-card.touch-preview {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.game-card.touch-preview .play-overlay {
opacity: 1;
}
.game-card.touch-preview .play-icon {
transform: scale(1.1);
}
}
/* Focus states for accessibility */
.game-card:focus-visible {
outline: 3px solid #6366f1;
outline-offset: 2px;
transform: translateY(-4px);
}
/* Live preview iframe - fills entire card */
.preview-iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
background: #1e1b4b;
}
.preview-iframe.loaded {
opacity: 1;
}
/* Fallback background when no preview */
.card-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%);
display: flex;
align-items: center;
justify-content: center;
}
.card-background::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { transform: translate(-30%, -30%); }
50% { transform: translate(30%, 30%); }
}
.bg-icon {
font-size: 3rem;
opacity: 0.5;
position: relative;
z-index: 1;
}
/* Loading indicator */
.preview-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #312e81 0%, #4c1d95 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: #a78bfa;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Gradient overlay for text readability */
.card-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.3) 0%,
transparent 30%,
transparent 50%,
rgba(0, 0, 0, 0.6) 100%
);
pointer-events: none;
z-index: 2;
}
/* Content layer - overlaid on top */
.card-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0.75rem;
z-index: 3;
pointer-events: none;
}
.card-content > * {
pointer-events: auto;
}
/* Top section - badges */
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.featured-badge {
background: #f59e0b;
color: white;
font-size: 0.65rem;
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
text-transform: uppercase;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.difficulty-badges {
display: flex;
gap: 0.25rem;
margin-left: auto;
}
.difficulty-badge {
font-size: 0.6rem;
font-weight: 600;
padding: 0.15rem 0.35rem;
border-radius: 0.2rem;
text-transform: capitalize;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.difficulty-easy {
background: #d1fae5;
color: #065f46;
}
.difficulty-medium {
background: #fef3c7;
color: #92400e;
}
.difficulty-hard {
background: #fee2e2;
color: #991b1b;
}
/* Bottom section - game info */
.card-bottom {
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.game-title {
font-size: 1rem;
font-weight: 700;
margin: 0 0 0.15rem;
color: white;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.game-creator {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.85);
margin: 0 0 0.35rem;
}
.game-stats {
display: flex;
gap: 0.75rem;
align-items: center;
}
.stat {
display: flex;
align-items: center;
gap: 0.2rem;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.9);
}
.stat-icon {
font-size: 0.75rem;
}
.rating-count {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.7);
margin-left: 0.1rem;
}
/* Favorite button */
.favorite-btn {
background: rgba(255, 255, 255, 0.15);
border: none;
padding: 0.2rem 0.4rem;
cursor: pointer;
border-radius: 0.4rem;
transition: all 0.2s;
margin-left: auto;
backdrop-filter: blur(4px);
}
.favorite-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.favorite-btn.favorited {
background: rgba(239, 68, 68, 0.3);
}
.favorite-btn .stat-icon {
font-size: 0.9rem;
}
/* Play overlay on hover/touch */
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.play-overlay.visible {
opacity: 1;
}
.play-icon {
width: 50px;
height: 50px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: #6366f1;
padding-left: 4px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
transition: transform 0.2s;
}
.play-icon::before {
content: '\25B6';
}
.play-icon.active {
width: auto;
padding: 0.75rem 1.25rem;
border-radius: 2rem;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.play-icon.active::before {
content: '';
}
/* Touch hint for mobile */
.touch-hint {
position: absolute;
bottom: 3.5rem;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
font-size: 0.65rem;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
z-index: 4;
pointer-events: none;
}
@media (hover: hover) {
.touch-hint {
display: none;
}
}
/* Mobile adjustments */
@media (max-width: 480px) {
.game-card {
aspect-ratio: 16 / 9;
}
.card-content {
padding: 0.5rem;
}
.game-title {
font-size: 0.9rem;
}
.game-creator {
font-size: 0.7rem;
}
.stat {
font-size: 0.75rem;
}
.play-icon {
width: 44px;
height: 44px;
font-size: 1rem;
}
.play-icon.active {
font-size: 0.75rem;
padding: 0.6rem 1rem;
}
}
/* Large touch targets */
@media (pointer: coarse) {
.favorite-btn {
padding: 0.3rem 0.5rem;
min-height: 32px;
}
}
+295
View File
@@ -0,0 +1,295 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Link } from 'react-router-dom';
import './GameCard.css';
export default function GameCard({ game, onToggleFavorite = null, showCreator = true, showPreview = false, gameCode = null }) {
const [isHovering, setIsHovering] = useState(false);
const [isTouchPreview, setIsTouchPreview] = useState(false);
const [localFavorited, setLocalFavorited] = useState(game.isFavorited);
const [localFavoriteCount, setLocalFavoriteCount] = useState(game.favoriteCount || 0);
const [previewLoaded, setPreviewLoaded] = useState(false);
const iframeRef = useRef(null);
const avgRating = game.averageRating ? game.averageRating.toFixed(1) : '-';
const handleFavoriteClick = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!onToggleFavorite) return;
const newFavorited = !localFavorited;
setLocalFavorited(newFavorited);
setLocalFavoriteCount(prev => newFavorited ? prev + 1 : Math.max(0, prev - 1));
try {
await onToggleFavorite(game.id);
} catch (error) {
setLocalFavorited(!newFavorited);
setLocalFavoriteCount(prev => newFavorited ? prev - 1 : prev + 1);
}
};
const supportsHover = typeof window !== 'undefined' &&
window.matchMedia('(hover: hover)').matches;
useEffect(() => {
setIsTouchPreview(false);
setPreviewLoaded(false);
}, [game.id]);
// Load preview iframe with muted audio
useEffect(() => {
if (!showPreview || !gameCode || !iframeRef.current) return;
const iframe = iframeRef.current;
// Inject code that mutes audio AND auto-plays the game
const mutedCode = gameCode.replace(
'</head>',
`<script>
// Mute all audio for preview - complete mock with all methods
window.AudioContext = window.webkitAudioContext = function() {
var noop = function() { return this; };
var mockParam = { value: 0, setValueAtTime: noop, linearRampToValueAtTime: noop, exponentialRampToValueAtTime: noop, setTargetAtTime: noop, cancelScheduledValues: noop };
var mockNode = { connect: noop, disconnect: noop, start: noop, stop: noop, frequency: mockParam, gain: mockParam, Q: mockParam, detune: mockParam };
return {
createOscillator: function() { return mockNode; },
createGain: function() { return { connect: noop, disconnect: noop, gain: mockParam }; },
createBiquadFilter: function() { return mockNode; },
createBuffer: function(c, l, r) { return { getChannelData: function() { return new Float32Array(l || 1); }, numberOfChannels: c, length: l, sampleRate: r }; },
createBufferSource: function() { return { connect: noop, disconnect: noop, start: noop, stop: noop, buffer: null, loop: false, playbackRate: mockParam }; },
createAnalyser: function() { return { connect: noop, fftSize: 2048, getByteFrequencyData: noop, getByteTimeDomainData: noop }; },
createConvolver: function() { return mockNode; },
createDelay: function() { return { connect: noop, delayTime: mockParam }; },
createDynamicsCompressor: function() { return mockNode; },
decodeAudioData: function(b, s, e) { s && s({ duration: 0, numberOfChannels: 1 }); return Promise.resolve({ duration: 0 }); },
destination: mockNode,
currentTime: 0,
sampleRate: 44100,
state: 'running',
resume: function() { return Promise.resolve(); },
suspend: function() { return Promise.resolve(); },
close: function() { return Promise.resolve(); }
};
};
window.Audio = function() {
return { play: function() { return Promise.resolve(); }, pause: noop, load: noop, volume: 0, muted: true, currentTime: 0, duration: 0, src: '' };
};
window.setMusicEnabled = function() {};
window.setSfxEnabled = function() {};
window.pauseMusic = function() {};
window.resumeMusic = function() {};
// Auto-play simulation - starts game and simulates gameplay
(function() {
const canvas = document.querySelector('canvas');
const keys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space', ' ', 'w', 'a', 's', 'd'];
function simulateKey(key) {
const evt = new KeyboardEvent('keydown', { key: key, code: key === ' ' ? 'Space' : 'Key' + key.toUpperCase(), bubbles: true });
document.dispatchEvent(evt);
window.dispatchEvent(evt);
if (canvas) canvas.dispatchEvent(evt);
}
function simulateClick(x, y) {
const target = canvas || document.body;
const rect = target.getBoundingClientRect ? target.getBoundingClientRect() : { left: 0, top: 0 };
const evt = new MouseEvent('click', { clientX: x || rect.left + 100, clientY: y || rect.top + 100, bubbles: true });
target.dispatchEvent(evt);
// Also try touch
if (window.TouchEvent) {
try {
const touch = new Touch({ identifier: 1, target: target, clientX: x || 100, clientY: y || 100 });
const touchEvt = new TouchEvent('touchstart', { touches: [touch], bubbles: true });
target.dispatchEvent(touchEvt);
} catch(e) {}
}
}
// Start game after 1 second (click to dismiss title screen)
setTimeout(() => {
simulateClick(window.innerWidth / 2, window.innerHeight / 2);
simulateKey(' ');
simulateKey('Enter');
}, 1000);
// Start gameplay simulation after 2 seconds
setTimeout(() => {
let tick = 0;
setInterval(() => {
tick++;
// Simulate random gameplay inputs
const key = keys[Math.floor(Math.random() * keys.length)];
simulateKey(key);
// Occasional clicks
if (tick % 5 === 0) {
simulateClick(
Math.random() * window.innerWidth * 0.8 + window.innerWidth * 0.1,
Math.random() * window.innerHeight * 0.8 + window.innerHeight * 0.1
);
}
}, 200);
}, 2000);
// If game over detected, restart after a delay
const origAlert = window.alert;
window.alert = function() {};
window.confirm = function() { return true; };
})();
</script></head>`
);
const blob = new Blob([mutedCode], { type: 'text/html' });
const url = URL.createObjectURL(blob);
iframe.src = url;
const handleLoad = () => {
setPreviewLoaded(true);
URL.revokeObjectURL(url);
};
iframe.addEventListener('load', handleLoad);
return () => {
iframe.removeEventListener('load', handleLoad);
URL.revokeObjectURL(url);
};
}, [showPreview, gameCode]);
const handleTouchStart = useCallback((e) => {
if (supportsHover) return;
if (!isTouchPreview) {
e.preventDefault();
setIsTouchPreview(true);
}
}, [supportsHover, isTouchPreview]);
const handleClick = useCallback((e) => {
if (!supportsHover && !isTouchPreview) {
e.preventDefault();
setIsTouchPreview(true);
}
}, [supportsHover, isTouchPreview]);
useEffect(() => {
if (!isTouchPreview) return;
const handleOutsideClick = (e) => {
if (!e.target.closest('.game-card')) {
setIsTouchPreview(false);
}
};
document.addEventListener('touchstart', handleOutsideClick);
document.addEventListener('click', handleOutsideClick);
return () => {
document.removeEventListener('touchstart', handleOutsideClick);
document.removeEventListener('click', handleOutsideClick);
};
}, [isTouchPreview]);
const showOverlay = isHovering || isTouchPreview;
const hasPreview = showPreview && gameCode;
return (
<Link
to={`/play/${game.id}`}
className={`game-card ${isTouchPreview ? 'touch-preview' : ''} ${hasPreview ? 'has-preview' : ''}`}
onMouseEnter={() => supportsHover && setIsHovering(true)}
onMouseLeave={() => supportsHover && setIsHovering(false)}
onTouchStart={handleTouchStart}
onClick={handleClick}
>
<div className="game-card-inner">
{/* Live preview iframe - fills entire card */}
{hasPreview && (
<iframe
ref={iframeRef}
className={`preview-iframe ${previewLoaded ? 'loaded' : ''}`}
sandbox="allow-scripts"
title={`Preview of ${game.title}`}
/>
)}
{/* Fallback/loading background when no preview */}
{!hasPreview && (
<div className="card-background">
<span className="bg-icon">🎮</span>
</div>
)}
{/* Loading indicator */}
{hasPreview && !previewLoaded && (
<div className="preview-loading">
<div className="loading-spinner"></div>
</div>
)}
{/* Overlay gradient for text readability */}
<div className="card-overlay"></div>
{/* All info overlaid on top */}
<div className="card-content">
{/* Top badges */}
<div className="card-top">
{game.isFeatured && <span className="featured-badge">Featured</span>}
{game.difficultyTags && game.difficultyTags.length > 0 && (
<div className="difficulty-badges">
{game.difficultyTags.map(d => (
<span key={d} className={`difficulty-badge difficulty-${d}`}>
{d}
</span>
))}
</div>
)}
</div>
{/* Bottom info */}
<div className="card-bottom">
<h3 className="game-title">{game.title}</h3>
{showCreator && (
<p className="game-creator">by {game.creatorDisplayName || game.creatorUsername}</p>
)}
<div className="game-stats">
<span className="stat">
<span className="stat-icon"></span>
{game.playCount.toLocaleString()}
</span>
<span className="stat">
<span className="stat-icon"></span>
{avgRating}
{game.totalRatings > 0 && (
<span className="rating-count">({game.totalRatings})</span>
)}
</span>
{onToggleFavorite && (
<button
className={`stat favorite-btn ${localFavorited ? 'favorited' : ''}`}
onClick={handleFavoriteClick}
aria-label={localFavorited ? 'Remove from favorites' : 'Add to favorites'}
>
<span className="stat-icon">{localFavorited ? '❤️' : '🤍'}</span>
{localFavoriteCount > 0 && localFavoriteCount.toLocaleString()}
</button>
)}
</div>
</div>
{/* Play overlay on hover/touch */}
<div className={`play-overlay ${showOverlay ? 'visible' : ''}`}>
<span className={`play-icon ${isTouchPreview ? 'active' : ''}`}>
{isTouchPreview ? 'TAP TO PLAY' : ''}
</span>
</div>
</div>
{/* Touch hint for mobile */}
{!supportsHover && !isTouchPreview && (
<div className="touch-hint">
<span>Tap to preview</span>
</div>
)}
</div>
</Link>
);
}
+104
View File
@@ -0,0 +1,104 @@
.guest-banner {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 1px solid #f59e0b;
border-radius: 0.75rem;
padding: 1rem;
margin-bottom: 1rem;
}
.guest-banner-content {
display: flex;
align-items: center;
gap: 0.75rem;
}
.guest-icon {
font-size: 1.5rem;
}
.guest-text {
flex: 1;
}
.guest-main {
font-weight: 600;
color: #92400e;
margin: 0;
font-size: 0.95rem;
}
.guest-sub {
color: #b45309;
margin: 0;
font-size: 0.85rem;
}
.guest-benefits {
margin: 0.75rem 0 0;
padding-top: 0.75rem;
border-top: 1px solid rgba(245, 158, 11, 0.3);
font-size: 0.8rem;
color: #92400e;
text-align: center;
}
.btn-signup {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.85rem;
text-decoration: none;
white-space: nowrap;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-signup:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
/* Compact version for header/toolbar */
.guest-banner.compact {
background: rgba(254, 243, 199, 0.9);
border: none;
border-radius: 2rem;
padding: 0.35rem 0.75rem;
margin: 0;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
}
.guest-tokens {
color: #92400e;
font-weight: 500;
}
.guest-signup-link {
color: #6366f1;
font-weight: 600;
text-decoration: none;
}
.guest-signup-link:hover {
text-decoration: underline;
}
@media (max-width: 480px) {
.guest-banner-content {
flex-wrap: wrap;
}
.btn-signup {
width: 100%;
text-align: center;
margin-top: 0.5rem;
}
.guest-text {
flex: 1 1 calc(100% - 3rem);
}
}
+31
View File
@@ -0,0 +1,31 @@
import { Link } from 'react-router-dom';
import './GuestBanner.css';
export default function GuestBanner({ tokensBalance, compact = false }) {
if (compact) {
return (
<div className="guest-banner compact">
<span className="guest-tokens">{tokensBalance || 0} tokens</span>
<Link to="/register" className="guest-signup-link">Sign up free</Link>
</div>
);
}
return (
<div className="guest-banner">
<div className="guest-banner-content">
<span className="guest-icon">👋</span>
<div className="guest-text">
<p className="guest-main">Playing as Guest</p>
<p className="guest-sub">{tokensBalance || 0} free tokens remaining today</p>
</div>
<Link to="/register" className="btn btn-signup">
Sign Up Free
</Link>
</div>
<p className="guest-benefits">
Create games, save favorites, earn XP & achievements!
</p>
</div>
);
}
+607
View File
@@ -0,0 +1,607 @@
.header {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
padding: 0.5rem 1rem;
position: -webkit-sticky; /* Safari iOS */
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
/* Ensure sticky works on mobile */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
/* Static header for game play pages - no sticky */
.header.header-static {
position: relative;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
height: 44px;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: white;
font-weight: 700;
font-size: 1.25rem;
flex-shrink: 0;
}
.logo-accent {
color: #fbbf24;
}
.logo-icon {
font-size: 1.5rem;
}
/* Desktop Navigation */
.desktop-nav {
display: flex;
gap: 0.5rem;
flex: 1;
justify-content: center;
}
.nav-link {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
font-weight: 500;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: all 0.2s;
font-size: 0.875rem;
white-space: nowrap;
}
.nav-link:hover {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.nav-create {
background: rgba(255, 255, 255, 0.2);
}
.nav-teacher {
background: rgba(16, 185, 129, 0.3);
color: white;
}
.nav-admin {
background: rgba(239, 68, 68, 0.3);
color: white;
}
.nav-leaderboard {
background: rgba(251, 191, 36, 0.3);
color: white;
}
/* Header Actions */
.header-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.token-display {
display: flex;
align-items: center;
gap: 0.25rem;
background: rgba(255, 255, 255, 0.2);
padding: 0.35rem 0.6rem;
border-radius: 2rem;
font-weight: 600;
color: white;
font-size: 0.875rem;
}
.token-icon {
font-size: 1rem;
}
.token-display-clickable {
border: none;
cursor: pointer;
transition: all 0.2s;
}
.token-display-clickable:hover {
background: rgba(255, 255, 255, 0.35);
}
.avatar-link {
display: flex;
align-items: center;
}
.avatar-link .avatar-svg {
border: 2px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.avatar-link:hover .avatar-svg {
border-color: white;
}
/* Combined profile link with avatar + level */
.header-profile-link {
display: flex;
align-items: center;
gap: 0.35rem;
text-decoration: none;
padding: 0.2rem 0.5rem 0.2rem 0.2rem;
border-radius: 2rem;
background: rgba(255, 255, 255, 0.1);
transition: background 0.2s;
}
.header-profile-link:hover {
background: rgba(255, 255, 255, 0.2);
}
.header-profile-link .avatar-svg {
border: 2px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.header-profile-link:hover .avatar-svg {
border-color: white;
}
.header-level-badge {
color: #fbbf24;
font-size: 0.7rem;
font-weight: 700;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
/* Desktop/Mobile visibility helpers */
.desktop-only {
display: flex;
align-items: center;
}
.mobile-only {
display: none;
}
/* Hamburger Button */
.hamburger-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 44px;
height: 44px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.hamburger-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.hamburger-icon {
font-size: 1.5rem;
}
/* Mobile Menu Overlay */
.mobile-menu-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 200;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.mobile-menu {
position: fixed;
top: 0;
right: 0;
width: min(300px, 85vw);
height: 100vh;
background: var(--card-bg, #1a1a2e);
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
animation: slideIn 0.3s ease;
overflow-y: auto;
display: flex;
flex-direction: column;
}
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
.mobile-menu-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
}
.mobile-menu-title {
font-size: 1.25rem;
font-weight: 700;
color: white;
}
.mobile-menu-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 1.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-menu-close:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Mobile Menu User Info */
.mobile-menu-user {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: rgba(99, 102, 241, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.mobile-user-info {
display: flex;
flex-direction: column;
}
.mobile-user-name {
font-weight: 600;
color: var(--text-primary, white);
}
.mobile-user-level {
font-size: 0.8rem;
color: #fbbf24;
font-weight: 500;
}
.mobile-user-tokens {
font-size: 0.8rem;
color: var(--text-secondary, #94a3b8);
display: flex;
align-items: center;
gap: 0.25rem;
margin-top: 0.15rem;
}
.mobile-user-tokens .token-icon {
font-size: 0.9rem;
}
/* Mobile Menu Links */
.mobile-menu-links {
flex: 1;
padding: 0.5rem 0;
}
.mobile-nav-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
color: var(--text-primary, white);
text-decoration: none;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s;
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
}
.mobile-nav-link:hover,
.mobile-nav-link:active {
background: rgba(99, 102, 241, 0.15);
}
.nav-icon {
font-size: 1.25rem;
width: 28px;
text-align: center;
}
.mobile-menu-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 0.5rem 0;
}
.mobile-nav-admin {
color: #ef4444;
}
.mobile-nav-signup {
background: rgba(99, 102, 241, 0.2);
}
/* Guest signup button in header */
.guest-signup-btn {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%) !important;
color: #1e293b !important;
font-weight: 600;
border: none;
box-shadow: 0 2px 8px rgba(251, 191, 36, 0.4);
animation: pulse-signup 2s ease-in-out infinite;
}
.guest-signup-btn:hover {
background: linear-gradient(135deg, #fcd34d 0%, #fbbf24 100%) !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(251, 191, 36, 0.5);
}
@keyframes pulse-signup {
0%, 100% { box-shadow: 0 2px 8px rgba(251, 191, 36, 0.4); }
50% { box-shadow: 0 2px 16px rgba(251, 191, 36, 0.6); }
}
.mobile-logout {
color: #ef4444;
}
/* XP display in header */
.header .xp-display-compact {
background: rgba(255, 255, 255, 0.15);
padding: 0.25rem 0.6rem;
border-radius: 2rem;
}
.header .xp-display-compact .level-badge {
background: rgba(255, 255, 255, 0.25);
color: white;
}
.header .xp-bar-mini {
background: rgba(255, 255, 255, 0.3);
}
.header .xp-bar-mini .xp-fill {
background: #fbbf24;
}
/* Mobile Responsive */
@media (max-width: 900px) {
.desktop-nav {
display: none;
}
}
@media (max-width: 768px) {
.header {
padding: 0.5rem 0.75rem;
}
.header-content {
height: 40px;
}
.desktop-only {
display: none !important;
}
.mobile-only {
display: flex;
}
.logo {
font-size: 1.1rem;
}
.logo-icon {
font-size: 1.25rem;
}
.logo-text {
display: none;
}
.token-display {
padding: 0.3rem 0.5rem;
font-size: 0.8rem;
}
.token-icon {
font-size: 0.9rem;
}
.hamburger-btn {
width: 40px;
height: 40px;
}
.btn-sm {
padding: 0.35rem 0.6rem;
font-size: 0.8rem;
}
}
/* Ultra-narrow screens (Galaxy Fold outer, small phones) */
@media (max-width: 360px) {
.header {
padding: 0.4rem 0.5rem;
}
.header-content {
gap: 0.25rem;
height: 36px;
}
.logo {
min-height: 36px;
}
.logo-icon {
font-size: 1.1rem;
}
/* Hide token display on very narrow screens - access via menu */
.token-display {
display: none;
}
/* Hide guest signup button - can sign up from menu */
.guest-signup-btn {
display: none;
}
/* Compact header actions */
.header-actions {
gap: 0.25rem;
}
/* Make hamburger compact but tappable */
.hamburger-btn {
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
flex-shrink: 0;
}
.hamburger-icon {
font-size: 1.25rem;
}
}
/* Extra narrow (Galaxy Fold outer ~260px) */
@media (max-width: 280px) {
.header {
padding: 0.35rem 0.4rem;
}
.header-content {
gap: 0.2rem;
height: 32px;
}
.logo-icon {
font-size: 1rem;
}
.hamburger-btn {
width: 32px;
height: 32px;
min-width: 32px;
min-height: 32px;
}
.hamburger-icon {
font-size: 1.1rem;
}
}
/* Touch-friendly styles */
@media (hover: none), (pointer: coarse) {
.nav-link,
.mobile-nav-link {
min-height: 44px;
}
.token-display-clickable {
min-height: 40px;
min-width: 60px;
}
.hamburger-btn {
min-width: 44px;
min-height: 44px;
}
.logo {
min-height: 44px;
}
}
/* Active states for touch devices */
@media (hover: none) {
.nav-link:active,
.mobile-nav-link:active {
background: rgba(255, 255, 255, 0.25);
transform: scale(0.98);
}
.token-display-clickable:active {
background: rgba(255, 255, 255, 0.4);
transform: scale(0.98);
}
.hamburger-btn:active {
background: rgba(255, 255, 255, 0.4);
transform: scale(0.95);
}
/* Disable iOS tap highlight */
.nav-link,
.mobile-nav-link,
.token-display-clickable,
.hamburger-btn,
.logo {
-webkit-tap-highlight-color: transparent;
}
}
/* Focus states for accessibility */
.nav-link:focus-visible,
.mobile-nav-link:focus-visible,
.token-display-clickable:focus-visible,
.hamburger-btn:focus-visible,
.logo:focus-visible {
outline: 2px solid white;
outline-offset: 2px;
}
/* Skip to content link */
.skip-to-content {
position: absolute;
left: -9999px;
z-index: 999;
padding: 1em;
background-color: black;
color: white;
opacity: 0;
}
.skip-to-content:focus {
left: 0;
opacity: 1;
}
+255
View File
@@ -0,0 +1,255 @@
import { useState, useEffect } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import NotificationBell from './NotificationBell';
import TokenHelpModal from './TokenHelpModal';
import { AvatarMini, DEFAULT_AVATAR } from './AvatarBuilder';
import './Header.css';
export default function Header() {
const { user, isAuthenticated, logout } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [showTokenHelp, setShowTokenHelp] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Close mobile menu on route change
useEffect(() => {
setMobileMenuOpen(false);
}, [location.pathname]);
// Prevent body scroll when menu is open
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [mobileMenuOpen]);
const handleLogout = () => {
logout();
setMobileMenuOpen(false);
navigate('/');
};
const closeMobileMenu = () => {
setMobileMenuOpen(false);
};
// Disable sticky header on game play pages
const isPlayPage = location.pathname.startsWith('/play/');
return (
<header className={`header ${isPlayPage ? 'header-static' : ''}`}>
<a href="#main-content" className="skip-to-content">
Skip to main content
</a>
<div className="header-content">
{/* Logo - always visible */}
<Link to="/" className="logo">
<span className="logo-icon">🎮</span>
<span className="logo-text">Gamer<span className="logo-accent">Comp</span></span>
</Link>
{/* Desktop Navigation */}
<nav className="nav desktop-nav">
<Link to="/arcade" className="nav-link">Arcade</Link>
<Link to="/leaderboard" className="nav-link nav-leaderboard">Leaderboard</Link>
{isAuthenticated && !user?.isGuest && (
<>
<Link to="/create" className="nav-link nav-create">Create</Link>
<Link to="/dashboard" className="nav-link">My Games</Link>
<Link to="/collections" className="nav-link">Collections</Link>
<Link to="/classrooms" className="nav-link nav-classrooms">Classrooms</Link>
{(user?.role === 'teacher' || user?.role === 'admin' || user?.isTeacher) && (
<Link to="/teacher" className="nav-link nav-teacher">Teacher</Link>
)}
{user?.role === 'admin' && (
<Link to="/admin" className="nav-link nav-admin">Admin</Link>
)}
</>
)}
</nav>
{/* Header Actions - compact on mobile */}
<div className="header-actions">
{isAuthenticated ? (
<>
{/* Token Display */}
<button
className="token-display token-display-clickable"
onClick={() => setShowTokenHelp(true)}
title="Click to learn about tokens"
>
<span className="token-icon">🪙</span>
<span className="token-count">{user?.tokensBalance ?? 0}</span>
</button>
{/* Sign Up button for guests */}
{user?.isGuest && (
<Link to="/register" className="btn btn-primary btn-sm guest-signup-btn">
Sign Up
</Link>
)}
{/* Notification Bell - not for guests */}
{!user?.isGuest && (
<NotificationBell />
)}
{/* Avatar + Level - links to profile on desktop */}
{!user?.isGuest && (
<Link to="/profile" className="header-profile-link desktop-only" title={`Level ${user?.level || 1}${user?.levelName || 'Newbie'}${user?.xp || 0} XP\nClick to view your profile`}>
<AvatarMini data={user?.avatarData || DEFAULT_AVATAR} size={32} />
<span className="header-level-badge">Lv.{user?.level || 1}</span>
</Link>
)}
{/* Logout - desktop only */}
<button onClick={handleLogout} className="btn btn-secondary btn-sm desktop-only">
Logout
</button>
{/* Hamburger Menu Button - mobile only */}
<button
className="hamburger-btn mobile-only"
onClick={() => setMobileMenuOpen(true)}
aria-label="Open menu"
>
<span className="hamburger-icon"></span>
</button>
<TokenHelpModal
isOpen={showTokenHelp}
onClose={() => setShowTokenHelp(false)}
/>
</>
) : (
<>
<Link to="/login" className="btn btn-secondary btn-sm">Login</Link>
<Link to="/register" className="btn btn-primary btn-sm">Sign Up</Link>
{/* Hamburger for non-logged in users */}
<button
className="hamburger-btn mobile-only"
onClick={() => setMobileMenuOpen(true)}
aria-label="Open menu"
>
<span className="hamburger-icon"></span>
</button>
</>
)}
</div>
</div>
{/* Mobile Menu Overlay */}
{mobileMenuOpen && (
<div className="mobile-menu-overlay" onClick={closeMobileMenu}>
<nav className="mobile-menu" onClick={e => e.stopPropagation()}>
<div className="mobile-menu-header">
<span className="mobile-menu-title">Menu</span>
<button
className="mobile-menu-close"
onClick={closeMobileMenu}
aria-label="Close menu"
>
</button>
</div>
{/* User info at top of menu */}
{isAuthenticated && (
<div className="mobile-menu-user">
<AvatarMini data={user?.avatarData || DEFAULT_AVATAR} size={48} />
<div className="mobile-user-info">
<span className="mobile-user-name">{user?.displayName || user?.username}</span>
{!user?.isGuest && (
<span className="mobile-user-level">Lv.{user?.level || 1} {user?.levelName || 'Newbie'} {(user?.xp || 0).toLocaleString()} XP</span>
)}
<span className="mobile-user-tokens">
<span className="token-icon">🪙</span> {user?.tokensBalance ?? 0} tokens
</span>
</div>
</div>
)}
<div className="mobile-menu-links">
<Link to="/arcade" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">🎮</span> Arcade
</Link>
<Link to="/leaderboard" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">🏆</span> Leaderboard
</Link>
{isAuthenticated && user?.isGuest && (
<>
<div className="mobile-menu-divider" />
<Link to="/register" className="mobile-nav-link mobile-nav-signup" onClick={closeMobileMenu}>
<span className="nav-icon"></span> Sign Up to Create Games
</Link>
</>
)}
{isAuthenticated && !user?.isGuest && (
<>
<div className="mobile-menu-divider" />
<Link to="/create" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon"></span> Create New Game
</Link>
<Link to="/dashboard" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">🎮</span> My Games
</Link>
<Link to="/collections" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">📁</span> My Collections
</Link>
<Link to="/achievements" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">🏅</span> My Achievements
</Link>
<Link to="/classrooms" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">🏫</span> Classrooms
</Link>
<Link to="/profile" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">👤</span> Profile
</Link>
{(user?.role === 'teacher' || user?.role === 'admin' || user?.isTeacher) && (
<Link to="/teacher" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">📚</span> Teacher Console
</Link>
)}
{user?.role === 'admin' && (
<Link to="/admin" className="mobile-nav-link mobile-nav-admin" onClick={closeMobileMenu}>
<span className="nav-icon"></span> Admin
</Link>
)}
</>
)}
<div className="mobile-menu-divider" />
{isAuthenticated ? (
<button className="mobile-nav-link mobile-logout" onClick={handleLogout}>
<span className="nav-icon">🚪</span> Logout
</button>
) : (
<>
<Link to="/login" className="mobile-nav-link" onClick={closeMobileMenu}>
<span className="nav-icon">🔑</span> Login
</Link>
<Link to="/register" className="mobile-nav-link mobile-nav-signup" onClick={closeMobileMenu}>
<span className="nav-icon"></span> Sign Up
</Link>
</>
)}
</div>
</nav>
</div>
)}
</header>
);
}
+159
View File
@@ -0,0 +1,159 @@
.install-prompt {
position: fixed;
bottom: 1rem;
left: 1rem;
right: 1rem;
max-width: 400px;
margin: 0 auto;
background: white;
border-radius: 1rem;
padding: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
z-index: 1000;
display: flex;
flex-direction: column;
gap: 0.75rem;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.install-content {
display: flex;
align-items: center;
gap: 0.75rem;
}
.install-icon {
font-size: 2rem;
flex-shrink: 0;
}
.install-text {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.install-text strong {
color: #1e293b;
font-size: 1rem;
}
.install-text span {
color: #64748b;
font-size: 0.85rem;
}
.install-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.install-dismiss {
background: transparent;
border: none;
color: #64748b;
padding: 0.5rem 1rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 0.5rem;
transition: all 0.2s;
}
.install-dismiss:hover {
background: #f1f5f9;
color: #1e293b;
}
.install-button {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
border: none;
padding: 0.5rem 1.25rem;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
border-radius: 0.5rem;
transition: all 0.2s;
}
.install-button:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
/* Adjust for safe area on mobile */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.install-prompt {
bottom: calc(1rem + env(safe-area-inset-bottom));
}
}
/* Touch-friendly styles */
@media (hover: none), (pointer: coarse) {
.install-dismiss {
min-height: 44px;
padding: 0.625rem 1.25rem;
font-size: 0.95rem;
}
.install-button {
min-height: 44px;
padding: 0.625rem 1.5rem;
font-size: 0.95rem;
}
.install-actions {
gap: 0.75rem;
}
}
/* Active states for touch devices */
@media (hover: none) {
.install-dismiss:active {
background: #e2e8f0;
transform: scale(0.98);
}
.install-button:active {
transform: scale(0.98);
opacity: 0.9;
}
/* Disable iOS tap highlight */
.install-dismiss,
.install-button {
-webkit-tap-highlight-color: transparent;
}
}
/* Hover-only styles for desktop */
@media (hover: hover) {
.install-dismiss:hover {
background: #f1f5f9;
color: #1e293b;
}
.install-button:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
}
/* Focus states for accessibility */
.install-dismiss:focus-visible,
.install-button:focus-visible {
outline: 3px solid #6366f1;
outline-offset: 2px;
}
+103
View File
@@ -0,0 +1,103 @@
import { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import './InstallPrompt.css';
export default function InstallPrompt() {
const { showInstallPrompt: authTrigger, dismissInstallPrompt } = useAuth();
const [deferredPrompt, setDeferredPrompt] = useState(null);
const [showPrompt, setShowPrompt] = useState(false);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// Check if already installed
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
return;
}
// Listen for the beforeinstallprompt event
const handleBeforeInstallPrompt = (e) => {
e.preventDefault();
setDeferredPrompt(e);
};
// Listen for successful install
const handleAppInstalled = () => {
setIsInstalled(true);
setShowPrompt(false);
setDeferredPrompt(null);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.addEventListener('appinstalled', handleAppInstalled);
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}, []);
// Show prompt when triggered by auth (login/register)
useEffect(() => {
if (authTrigger && deferredPrompt && !isInstalled) {
// Check if dismissed recently (don't show for 7 days after dismissal)
const dismissedAt = localStorage.getItem('pwa_install_dismissed');
if (dismissedAt) {
const daysSinceDismissal = (Date.now() - parseInt(dismissedAt)) / (1000 * 60 * 60 * 24);
if (daysSinceDismissal < 7) {
dismissInstallPrompt();
return;
}
}
setShowPrompt(true);
}
}, [authTrigger, deferredPrompt, isInstalled, dismissInstallPrompt]);
const handleInstall = async () => {
if (!deferredPrompt) return;
// Show the install prompt
deferredPrompt.prompt();
// Wait for the user's response
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setIsInstalled(true);
}
setDeferredPrompt(null);
setShowPrompt(false);
dismissInstallPrompt();
};
const handleDismiss = () => {
setShowPrompt(false);
dismissInstallPrompt();
localStorage.setItem('pwa_install_dismissed', Date.now().toString());
};
if (!showPrompt || isInstalled || !deferredPrompt) {
return null;
}
return (
<div className="install-prompt">
<div className="install-content">
<span className="install-icon">🎮</span>
<div className="install-text">
<strong>Install GamerComp</strong>
<span>Play games offline anytime!</span>
</div>
</div>
<div className="install-actions">
<button className="install-dismiss" onClick={handleDismiss}>
Not now
</button>
<button className="install-button" onClick={handleInstall}>
Install
</button>
</div>
</div>
);
}
@@ -0,0 +1,118 @@
.level-up-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.confetti-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
}
.confetti {
position: absolute;
top: -10px;
width: 10px;
height: 10px;
border-radius: 2px;
animation: confettiFall linear forwards;
}
@keyframes confettiFall {
0% {
transform: translateY(0) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
.level-up-modal {
background: white;
border-radius: 1.5rem;
padding: 2.5rem;
text-align: center;
max-width: 320px;
width: 90%;
animation: popIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
z-index: 1;
}
@keyframes popIn {
0% {
transform: scale(0.5);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.level-up-icon {
font-size: 4rem;
margin-bottom: 0.5rem;
animation: bounce 0.6s ease infinite alternate;
}
@keyframes bounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-10px);
}
}
.level-up-modal h2 {
margin: 0 0 0.5rem;
font-size: 2rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #f59e0b 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.level-up-level {
font-size: 1.5rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.25rem;
}
.level-up-name {
font-size: 1.25rem;
color: #6366f1;
font-weight: 600;
margin-bottom: 1rem;
}
.level-up-message {
color: #64748b;
margin-bottom: 1.5rem;
}
.level-up-modal .btn {
min-width: 120px;
}
@@ -0,0 +1,65 @@
import { useState, useEffect } from 'react';
import './LevelUpCelebration.css';
const LEVEL_ICONS = {
1: '🌱', 2: '🎮', 3: '🎯', 4: '⭐', 5: '🔥',
6: '💎', 7: '🏆', 8: '👑', 9: '🌟', 10: '🎖️'
};
export default function LevelUpCelebration({ level, levelName, onClose }) {
const [visible, setVisible] = useState(true);
const [confetti, setConfetti] = useState([]);
useEffect(() => {
// Generate confetti particles
const particles = Array.from({ length: 50 }, (_, i) => ({
id: i,
left: Math.random() * 100,
delay: Math.random() * 0.5,
duration: 1 + Math.random() * 2,
color: ['#6366f1', '#8b5cf6', '#f59e0b', '#10b981', '#ec4899'][Math.floor(Math.random() * 5)]
}));
setConfetti(particles);
// Auto close after 4 seconds
const timer = setTimeout(() => {
setVisible(false);
setTimeout(onClose, 300);
}, 4000);
return () => clearTimeout(timer);
}, [onClose]);
if (!visible) return null;
const icon = LEVEL_ICONS[level] || '⭐';
return (
<div className="level-up-overlay" onClick={onClose}>
<div className="confetti-container">
{confetti.map(p => (
<div
key={p.id}
className="confetti"
style={{
left: `${p.left}%`,
animationDelay: `${p.delay}s`,
animationDuration: `${p.duration}s`,
backgroundColor: p.color
}}
/>
))}
</div>
<div className="level-up-modal" onClick={e => e.stopPropagation()}>
<div className="level-up-icon">{icon}</div>
<h2>Level Up!</h2>
<div className="level-up-level">Level {level}</div>
<div className="level-up-name">{levelName}</div>
<p className="level-up-message">Keep creating awesome games!</p>
<button className="btn btn-primary" onClick={onClose}>
Awesome!
</button>
</div>
</div>
);
}
@@ -0,0 +1,352 @@
.notification-bell {
position: relative;
}
.bell-button {
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
}
.bell-button:hover {
background: rgba(0, 0, 0, 0.05);
}
.bell-icon {
font-size: 1.25rem;
}
.bell-button.has-unread .bell-icon {
animation: ring 0.5s ease-in-out;
}
@keyframes ring {
0%, 100% { transform: rotate(0); }
25% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
75% { transform: rotate(10deg); }
}
.unread-badge {
position: absolute;
top: 0;
right: 0;
background: #ef4444;
color: white;
font-size: 0.65rem;
font-weight: 600;
min-width: 16px;
height: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
.notification-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 320px;
max-height: 420px;
background: white;
border-radius: 0.75rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
z-index: 1000;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e2e8f0;
}
.dropdown-header h3 {
margin: 0;
font-size: 0.95rem;
color: #1e293b;
}
.mark-all-read {
background: none;
border: none;
color: #6366f1;
font-size: 0.8rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.mark-all-read:hover {
background: #f0f0ff;
}
.mark-all-read:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.notification-list {
flex: 1;
overflow-y: auto;
}
.empty-notifications {
padding: 2rem;
text-align: center;
color: #64748b;
}
.empty-icon {
font-size: 2.5rem;
display: block;
margin-bottom: 0.5rem;
}
.empty-notifications p {
margin: 0;
}
.notification-item {
display: flex;
gap: 0.75rem;
padding: 0.875rem 1rem;
cursor: pointer;
transition: background 0.2s;
position: relative;
border-bottom: 1px solid #f1f5f9;
}
.notification-item:hover {
background: #f8fafc;
}
.notification-item.unread {
background: #faf5ff;
}
.notification-item.unread:hover {
background: #f5f3ff;
}
.notification-icon {
font-size: 1.5rem;
flex-shrink: 0;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: #f1f5f9;
border-radius: 50%;
}
.notification-item.unread .notification-icon {
background: #e9d5ff;
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-title {
margin: 0 0 0.25rem;
font-size: 0.9rem;
font-weight: 500;
color: #1e293b;
}
.notification-body {
margin: 0 0 0.25rem;
font-size: 0.8rem;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-time {
font-size: 0.7rem;
color: #94a3b8;
}
.unread-dot {
position: absolute;
top: 50%;
right: 12px;
transform: translateY(-50%);
width: 8px;
height: 8px;
background: #6366f1;
border-radius: 50%;
}
.dropdown-footer {
padding: 0.75rem 1rem;
text-align: center;
border-top: 1px solid #e2e8f0;
background: #f8fafc;
}
.dropdown-footer a {
color: #6366f1;
font-size: 0.85rem;
text-decoration: none;
}
.dropdown-footer a:hover {
text-decoration: underline;
}
/* Mobile responsive */
@media (max-width: 640px) {
.notification-dropdown {
position: fixed;
top: 60px;
left: 0;
right: 0;
width: 100%;
max-height: calc(100vh - 60px);
border-radius: 0;
}
}
/* Ultra-narrow screens (Galaxy Fold outer, small phones) */
@media (max-width: 360px) {
.bell-button {
min-width: 36px;
min-height: 36px;
width: 36px;
height: 36px;
padding: 0.4rem;
}
.bell-icon {
font-size: 1.1rem;
}
.unread-badge {
min-width: 14px;
height: 14px;
font-size: 0.6rem;
top: -2px;
right: -2px;
}
.notification-dropdown {
top: 52px;
max-height: calc(100vh - 52px);
}
}
/* Touch-friendly styles */
@media (hover: none), (pointer: coarse) {
.bell-button {
min-width: 44px;
min-height: 44px;
padding: 0.625rem;
}
.bell-icon {
font-size: 1.5rem;
}
.mark-all-read {
min-height: 36px;
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
}
.notification-item {
min-height: 60px;
padding: 1rem;
}
.dropdown-footer a {
display: inline-block;
min-height: 44px;
padding: 0.75rem 1rem;
line-height: 1.5;
}
}
/* Active states for touch devices */
@media (hover: none) {
.bell-button:active {
background: rgba(0, 0, 0, 0.1);
transform: scale(0.95);
}
.mark-all-read:active:not(:disabled) {
background: #ddd6fe;
transform: scale(0.98);
}
.notification-item:active {
background: #f1f5f9;
transform: scale(0.99);
}
.notification-item.unread:active {
background: #ede9fe;
}
.dropdown-footer a:active {
color: #4338ca;
background: #f1f5f9;
}
/* Disable iOS tap highlight */
.bell-button,
.mark-all-read,
.notification-item,
.dropdown-footer a {
-webkit-tap-highlight-color: transparent;
}
}
/* Hover-only styles for desktop */
@media (hover: hover) {
.bell-button:hover {
background: rgba(0, 0, 0, 0.05);
}
.mark-all-read:hover {
background: #f0f0ff;
}
.notification-item:hover {
background: #f8fafc;
}
.notification-item.unread:hover {
background: #f5f3ff;
}
.dropdown-footer a:hover {
text-decoration: underline;
}
}
/* Focus states for accessibility */
.bell-button:focus-visible,
.mark-all-read:focus-visible,
.notification-item:focus-visible,
.dropdown-footer a:focus-visible {
outline: 3px solid #6366f1;
outline-offset: 2px;
}
@@ -0,0 +1,164 @@
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import api from '../api';
import './NotificationBell.css';
export default function NotificationBell() {
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
loadNotifications();
// Poll for new notifications every 30 seconds
const interval = setInterval(loadNotifications, 30000);
return () => clearInterval(interval);
}, []);
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
async function loadNotifications() {
try {
const data = await api.getNotifications();
setNotifications(data.notifications || []);
setUnreadCount(data.unreadCount || 0);
} catch (err) {
console.error('Failed to load notifications:', err);
}
}
async function handleMarkRead(notificationId) {
try {
await api.markNotificationRead(notificationId);
setNotifications(prev =>
prev.map(n => n.id === notificationId ? { ...n, read_at: new Date() } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
} catch (err) {
console.error('Failed to mark notification read:', err);
}
}
async function handleMarkAllRead() {
setLoading(true);
try {
await api.markAllNotificationsRead();
setNotifications(prev => prev.map(n => ({ ...n, read_at: new Date() })));
setUnreadCount(0);
} catch (err) {
console.error('Failed to mark all read:', err);
} finally {
setLoading(false);
}
}
function getNotificationIcon(type) {
const icons = {
achievement: '🏆',
high_score: '🎯',
game_favorited: '💖',
new_follower: '👋',
game_remixed: '🔄',
game_ready: '🎮',
level_up: '⭐',
welcome: '🎮',
default: '📬'
};
return icons[type] || icons.default;
}
function formatTime(dateStr) {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString();
}
return (
<div className="notification-bell" ref={dropdownRef}>
<button
className={`bell-button ${unreadCount > 0 ? 'has-unread' : ''}`}
onClick={() => setIsOpen(!isOpen)}
aria-label={`Notifications${unreadCount > 0 ? ` (${unreadCount} unread)` : ''}`}
>
<span className="bell-icon">🔔</span>
{unreadCount > 0 && (
<span className="unread-badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
)}
</button>
{isOpen && (
<div className="notification-dropdown">
<div className="dropdown-header">
<h3>Notifications</h3>
{unreadCount > 0 && (
<button
className="mark-all-read"
onClick={handleMarkAllRead}
disabled={loading}
>
Mark all read
</button>
)}
</div>
<div className="notification-list">
{notifications.length === 0 ? (
<div className="empty-notifications">
<span className="empty-icon">📭</span>
<p>No notifications yet</p>
</div>
) : (
notifications.slice(0, 10).map(notification => (
<div
key={notification.id}
className={`notification-item ${!notification.read_at ? 'unread' : ''}`}
onClick={() => !notification.read_at && handleMarkRead(notification.id)}
>
<span className="notification-icon">
{getNotificationIcon(notification.type)}
</span>
<div className="notification-content">
<p className="notification-title">{notification.title}</p>
<p className="notification-body">{notification.body}</p>
<span className="notification-time">
{formatTime(notification.created_at)}
</span>
</div>
{!notification.read_at && <span className="unread-dot" />}
</div>
))
)}
</div>
{notifications.length > 10 && (
<div className="dropdown-footer">
<Link to="/notifications" onClick={() => setIsOpen(false)}>
View all notifications
</Link>
</div>
)}
</div>
)}
</div>
);
}
@@ -0,0 +1,52 @@
.offline-indicator {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 0.5rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
z-index: 9999;
font-size: 0.85rem;
font-weight: 500;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
.offline-indicator.offline {
background: #fef3c7;
color: #92400e;
}
.offline-indicator.online {
background: #d1fae5;
color: #065f46;
animation: slideDown 0.3s ease, fadeOut 0.5s ease 2.5s forwards;
}
@keyframes fadeOut {
to {
opacity: 0;
transform: translateY(-100%);
}
}
.offline-icon {
font-size: 1rem;
}
.offline-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -0,0 +1,41 @@
import { useState, useEffect } from 'react';
import './OfflineIndicator.css';
export default function OfflineIndicator() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [showReconnected, setShowReconnected] = useState(false);
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
setShowReconnected(true);
setTimeout(() => setShowReconnected(false), 3000);
};
const handleOffline = () => {
setIsOnline(false);
setShowReconnected(false);
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (isOnline && !showReconnected) {
return null;
}
return (
<div className={`offline-indicator ${isOnline ? 'online' : 'offline'}`}>
<span className="offline-icon">{isOnline ? '🟢' : '📡'}</span>
<span className="offline-text">
{isOnline ? 'Back online!' : 'You\'re offline - some features may be limited'}
</span>
</div>
);
}
@@ -0,0 +1,280 @@
.onboarding-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.95) 0%, rgba(139, 92, 246, 0.95) 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
padding: 1rem;
animation: fadeIn 0.3s ease;
overflow: hidden;
}
.onboarding-overlay.exiting {
animation: fadeOut 0.3s ease forwards;
}
.onboarding-modal {
background: white;
border-radius: 1.5rem;
padding: 2.5rem 2rem;
max-width: 420px;
width: 100%;
text-align: center;
position: relative;
animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.onboarding-skip {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
color: #94a3b8;
font-size: 0.9rem;
cursor: pointer;
padding: 0.5rem;
transition: color 0.2s;
}
.onboarding-skip:hover {
color: #64748b;
}
.onboarding-content {
padding: 1rem 0 1.5rem;
}
.onboarding-emoji {
font-size: 4rem;
display: block;
margin-bottom: 1rem;
animation: bounce 0.6s ease infinite alternate;
}
.onboarding-title {
font-size: 1.75rem;
font-weight: 700;
color: #1e293b;
margin: 0 0 0.75rem;
}
.onboarding-text {
font-size: 1.1rem;
color: #475569;
margin: 0 0 0.5rem;
line-height: 1.5;
}
.onboarding-subtext {
font-size: 0.9rem;
color: #94a3b8;
margin: 0;
font-style: italic;
}
.onboarding-dots {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.onboarding-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: none;
background: #e2e8f0;
cursor: pointer;
padding: 0;
transition: all 0.2s;
}
.onboarding-dot:hover {
background: #cbd5e1;
}
.onboarding-dot.active {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
transform: scale(1.2);
}
.onboarding-dot.completed {
background: #10b981;
}
.onboarding-actions {
display: flex;
justify-content: center;
}
.onboarding-btn {
min-width: 140px;
padding: 0.875rem 2rem;
font-size: 1.1rem;
border-radius: 2rem;
}
/* Confetti */
.confetti-particle {
position: absolute;
top: -20px;
border-radius: 2px;
animation: confettiFall linear forwards;
pointer-events: none;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes bounce {
from { transform: translateY(0); }
to { transform: translateY(-8px); }
}
@keyframes confettiFall {
0% {
transform: translateY(0) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
/* Mobile adjustments */
@media (max-width: 480px) {
.onboarding-modal {
padding: 2rem 1.5rem;
border-radius: 1rem;
margin: 0 0.5rem;
}
.onboarding-emoji {
font-size: 3rem;
}
.onboarding-title {
font-size: 1.5rem;
}
.onboarding-text {
font-size: 1rem;
}
.onboarding-btn {
width: 100%;
max-width: 200px;
}
}
/* Large screens */
@media (min-width: 768px) {
.onboarding-modal {
padding: 3rem 2.5rem;
max-width: 480px;
}
.onboarding-emoji {
font-size: 5rem;
}
.onboarding-title {
font-size: 2rem;
}
}
/* Touch-friendly styles */
@media (hover: none), (pointer: coarse) {
.onboarding-skip {
min-width: 44px;
min-height: 44px;
padding: 0.625rem;
font-size: 0.95rem;
}
.onboarding-dot {
width: 14px;
height: 14px;
/* Larger touch target with invisible padding */
padding: 8px;
box-sizing: content-box;
margin: -8px;
}
.onboarding-btn {
min-height: 48px;
padding: 1rem 2rem;
}
}
/* Active states for touch devices */
@media (hover: none) {
.onboarding-skip:active {
color: #475569;
background: rgba(0, 0, 0, 0.05);
transform: scale(0.98);
}
.onboarding-dot:active {
transform: scale(1.3);
}
.onboarding-btn:active {
transform: scale(0.98);
opacity: 0.9;
}
/* Disable iOS tap highlight */
.onboarding-skip,
.onboarding-dot,
.onboarding-btn {
-webkit-tap-highlight-color: transparent;
}
}
/* Hover-only styles for desktop */
@media (hover: hover) {
.onboarding-skip:hover {
color: #64748b;
}
.onboarding-dot:hover {
background: #cbd5e1;
}
}
/* Focus states for accessibility */
.onboarding-skip:focus-visible,
.onboarding-dot:focus-visible,
.onboarding-btn:focus-visible {
outline: 3px solid #6366f1;
outline-offset: 2px;
}
@@ -0,0 +1,160 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import api from '../api';
import './OnboardingTutorial.css';
const STEPS = [
{
id: 1,
title: 'Welcome to GamerComp!',
emoji: '🎮',
content: 'Your new favorite place to play and create awesome games!',
subtext: 'Made by kids, for kids'
},
{
id: 2,
title: 'Play Amazing Games',
emoji: '🕹️',
content: 'Browse the arcade and play games created by other players.',
subtext: 'Earn XP and climb the leaderboards!'
},
{
id: 3,
title: 'Create Your Own Games',
emoji: '✨',
content: 'Use our AI wizard to create your own games in minutes!',
subtext: 'No coding required - just describe your idea'
},
{
id: 4,
title: 'Earn Rewards & Level Up',
emoji: '🏆',
content: 'Collect achievements, earn XP, and become a Game Master!',
subtext: 'The more you play and create, the more you earn'
}
];
export default function OnboardingTutorial({ onComplete }) {
const [currentStep, setCurrentStep] = useState(0);
const [isExiting, setIsExiting] = useState(false);
const [confetti, setConfetti] = useState([]);
const navigate = useNavigate();
const step = STEPS[currentStep];
const isLastStep = currentStep === STEPS.length - 1;
useEffect(() => {
// Prevent body scroll while tutorial is open
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = '';
};
}, []);
const generateConfetti = () => {
const particles = Array.from({ length: 40 }, (_, i) => ({
id: i,
left: Math.random() * 100,
delay: Math.random() * 0.3,
duration: 1.5 + Math.random() * 1.5,
color: ['#6366f1', '#8b5cf6', '#f59e0b', '#10b981', '#ec4899', '#3b82f6'][
Math.floor(Math.random() * 6)
],
size: 6 + Math.random() * 8
}));
setConfetti(particles);
};
const handleNext = () => {
if (isLastStep) {
handleComplete();
} else {
setCurrentStep(prev => prev + 1);
}
};
const handleSkip = () => {
handleComplete();
};
const handleComplete = async () => {
generateConfetti();
// Save onboarding status to database - this is the source of truth
try {
await api.completeOnboarding();
} catch (err) {
// Log but don't block - we'll still update local state
console.warn('Failed to save onboarding status:', err.message);
}
// Show celebration briefly then exit
setTimeout(() => {
setIsExiting(true);
setTimeout(() => {
onComplete();
// Don't navigate if already on /arcade
if (window.location.pathname !== '/arcade') {
navigate('/arcade');
}
}, 300);
}, 1500);
};
return (
<div className={`onboarding-overlay ${isExiting ? 'exiting' : ''}`}>
{/* Confetti */}
{confetti.map(particle => (
<div
key={particle.id}
className="confetti-particle"
style={{
left: `${particle.left}%`,
backgroundColor: particle.color,
width: particle.size,
height: particle.size,
animationDelay: `${particle.delay}s`,
animationDuration: `${particle.duration}s`
}}
/>
))}
<div className="onboarding-modal">
{/* Skip button */}
{!isLastStep && (
<button className="onboarding-skip" onClick={handleSkip}>
Skip
</button>
)}
{/* Step content */}
<div className="onboarding-content">
<span className="onboarding-emoji">{step.emoji}</span>
<h1 className="onboarding-title">{step.title}</h1>
<p className="onboarding-text">{step.content}</p>
<p className="onboarding-subtext">{step.subtext}</p>
</div>
{/* Progress dots */}
<div className="onboarding-dots">
{STEPS.map((_, index) => (
<button
key={index}
className={`onboarding-dot ${index === currentStep ? 'active' : ''} ${
index < currentStep ? 'completed' : ''
}`}
onClick={() => setCurrentStep(index)}
/>
))}
</div>
{/* Navigation */}
<div className="onboarding-actions">
<button className="btn btn-primary onboarding-btn" onClick={handleNext}>
{isLastStep ? "Let's Go!" : 'Next'}
</button>
</div>
</div>
</div>
);
}
+279
View File
@@ -0,0 +1,279 @@
.prompt-refiner {
max-width: 700px;
margin: 0 auto;
width: 100%;
}
/* Offline state */
.refiner-offline {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem;
text-align: center;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
}
.refiner-offline .offline-icon {
font-size: 2rem;
}
.refiner-offline-actions {
display: flex;
gap: 0.75rem;
}
.refiner-fallback-banner {
background: rgba(234, 179, 8, 0.15);
border: 1px solid rgba(234, 179, 8, 0.3);
color: #fde047;
padding: 0.4rem 0.75rem;
border-radius: 8px;
font-size: 0.8rem;
text-align: center;
margin-bottom: 0.5rem;
}
/* IDEA PHASE */
.refiner-idea-phase {
display: flex;
flex-direction: column;
gap: 1rem;
}
.refiner-idea-phase h2 {
margin: 0;
font-size: 1.4rem;
}
.refiner-subtitle {
color: #aaa;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.free-tag {
background: #22c55e;
color: #fff;
font-size: 0.7rem;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-weight: 700;
letter-spacing: 0.5px;
}
.refiner-idea-phase textarea {
width: 100%;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
color: #eee;
padding: 0.8rem;
font-size: 0.95rem;
font-family: inherit;
resize: vertical;
}
.refiner-idea-phase textarea:focus {
outline: none;
border-color: rgba(100, 200, 255, 0.5);
}
.refiner-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.refiner-error {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
padding: 0.5rem 0.75rem;
border-radius: 8px;
font-size: 0.85rem;
}
/* CHAT PHASE */
.refiner-chat-phase {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h3 {
margin: 0;
font-size: 1.1rem;
}
.question-progress {
color: #888;
font-size: 0.85rem;
}
.chat-messages {
display: flex;
flex-direction: column;
gap: 0.6rem;
max-height: 400px;
overflow-y: auto;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
}
.chat-bubble {
padding: 0.6rem 0.8rem;
border-radius: 12px;
font-size: 0.9rem;
line-height: 1.4;
max-width: 85%;
word-wrap: break-word;
}
.bubble-label {
display: block;
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.2rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.user-idea {
background: rgba(100, 200, 255, 0.1);
border: 1px solid rgba(100, 200, 255, 0.2);
align-self: flex-end;
}
.user-idea .bubble-label {
color: rgba(100, 200, 255, 0.7);
}
.ai-bubble {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
align-self: flex-start;
}
.ai-bubble .bubble-label {
color: rgba(34, 197, 94, 0.7);
}
.user-bubble {
background: rgba(100, 200, 255, 0.15);
border: 1px solid rgba(100, 200, 255, 0.25);
align-self: flex-end;
}
.typing .typing-dots {
display: flex;
gap: 0.3rem;
}
.typing .typing-dots span {
width: 6px;
height: 6px;
background: #888;
border-radius: 50%;
animation: typingBounce 1.2s infinite;
}
.typing .typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.typing .typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typingBounce {
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
30% { opacity: 1; transform: translateY(-4px); }
}
.chat-input-area {
display: flex;
gap: 0.5rem;
}
.chat-input-area input {
flex: 1;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #eee;
padding: 0.6rem 0.8rem;
font-size: 0.9rem;
font-family: inherit;
}
.chat-input-area input:focus {
outline: none;
border-color: rgba(100, 200, 255, 0.5);
}
.chat-footer-actions {
display: flex;
justify-content: center;
}
/* REVIEW PHASE */
.refiner-review-phase {
display: flex;
flex-direction: column;
gap: 1rem;
}
.refiner-review-phase h2 {
margin: 0;
font-size: 1.3rem;
}
.spec-editor {
width: 100%;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
color: #eee;
padding: 0.8rem;
font-size: 0.9rem;
font-family: monospace;
resize: vertical;
line-height: 1.5;
}
.spec-editor:focus {
outline: none;
border-color: rgba(100, 200, 255, 0.5);
}
/* Mobile adjustments */
@media (max-width: 600px) {
.refiner-actions {
flex-direction: column;
}
.refiner-actions .btn {
width: 100%;
}
.chat-messages {
max-height: 300px;
}
.chat-bubble {
max-width: 90%;
}
}
+263
View File
@@ -0,0 +1,263 @@
import { useState, useEffect, useRef } from 'react';
import api from '../api';
import './PromptRefiner.css';
const PHASES = {
IDEA: 'idea',
CHAT: 'chat',
REVIEW: 'review'
};
export default function PromptRefiner({ onComplete, onSkip, styleId, styleName }) {
const [phase, setPhase] = useState(PHASES.IDEA);
const [gameIdea, setGameIdea] = useState('');
const [conversation, setConversation] = useState([]);
const [currentAnswer, setCurrentAnswer] = useState('');
const [refinedSpec, setRefinedSpec] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [questionCount, setQuestionCount] = useState(0);
const chatEndRef = useRef(null);
useEffect(() => {
if (chatEndRef.current) {
chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [conversation]);
async function handleStartChat() {
if (!gameIdea.trim()) {
setError('Please describe your game idea');
return;
}
setLoading(true);
setError('');
try {
const result = await api.refinePromptChat(gameIdea, []);
if (result.isComplete) {
setRefinedSpec(result.refinedSpec);
setPhase(PHASES.REVIEW);
} else {
setConversation([{ role: 'ai', text: result.question }]);
setQuestionCount(1);
setPhase(PHASES.CHAT);
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
async function handleSendAnswer() {
if (!currentAnswer.trim()) return;
const newConversation = [
...conversation,
{ role: 'user', text: currentAnswer.trim() }
];
setConversation(newConversation);
setCurrentAnswer('');
setLoading(true);
setError('');
try {
const result = await api.refinePromptChat(gameIdea, newConversation);
if (result.isComplete) {
setRefinedSpec(result.refinedSpec);
setPhase(PHASES.REVIEW);
} else {
setConversation(prev => [...prev, { role: 'ai', text: result.question }]);
setQuestionCount(prev => prev + 1);
}
} catch (err) {
setError(err.message);
// Remove the user message we added if the API failed
setConversation(conversation);
} finally {
setLoading(false);
}
}
function handleDoneEarly() {
// Build spec from conversation so far
let spec = `Game Style: ${styleName}\n`;
spec += `Idea: ${gameIdea}\n\n`;
spec += 'Details from conversation:\n';
for (let i = 0; i < conversation.length; i += 2) {
const question = conversation[i]?.text || '';
const answer = conversation[i + 1]?.text || '';
if (answer) {
spec += `- ${question}\n Answer: ${answer}\n`;
}
}
setRefinedSpec(spec);
setPhase(PHASES.REVIEW);
}
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (phase === PHASES.IDEA) {
handleStartChat();
} else if (phase === PHASES.CHAT) {
handleSendAnswer();
}
}
}
return (
<div className="prompt-refiner">
{/* IDEA PHASE */}
{phase === PHASES.IDEA && (
<div className="refiner-idea-phase">
<h2>Describe your game idea</h2>
<p className="refiner-subtitle">
Our AI will ask you a few questions to help flesh out the details.
<span className="free-tag">FREE</span>
</p>
<textarea
value={gameIdea}
onChange={(e) => setGameIdea(e.target.value)}
placeholder={`e.g., "A space shooter where you defend earth from aliens" or "A puzzle game with falling blocks"`}
rows={4}
maxLength={1000}
onKeyDown={handleKeyDown}
disabled={loading}
/>
{error && <div className="refiner-error">{error}</div>}
<div className="refiner-actions">
<button
className="btn btn-primary btn-lg"
onClick={handleStartChat}
disabled={loading || !gameIdea.trim()}
>
{loading ? 'Starting...' : 'Refine with AI (FREE)'}
</button>
<button className="btn btn-secondary" onClick={onSkip}>
Use guided questions
</button>
</div>
</div>
)}
{/* CHAT PHASE */}
{phase === PHASES.CHAT && (
<div className="refiner-chat-phase">
<div className="chat-header">
<h3>AI Game Designer</h3>
<span className="question-progress">Question {questionCount} of ~5</span>
</div>
<div className="chat-messages">
<div className="chat-bubble user-idea">
<span className="bubble-label">Your idea:</span>
{gameIdea}
</div>
{conversation.map((msg, i) => (
<div key={i} className={`chat-bubble ${msg.role === 'ai' ? 'ai-bubble' : 'user-bubble'}`}>
{msg.role === 'ai' && <span className="bubble-label">AI</span>}
{msg.text}
</div>
))}
{loading && (
<div className="chat-bubble ai-bubble typing">
<span className="typing-dots">
<span></span><span></span><span></span>
</span>
</div>
)}
<div ref={chatEndRef} />
</div>
<div className="chat-input-area">
<input
type="text"
value={currentAnswer}
onChange={(e) => setCurrentAnswer(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your answer..."
disabled={loading}
autoFocus
/>
<button
className="btn btn-primary"
onClick={handleSendAnswer}
disabled={loading || !currentAnswer.trim()}
>
Answer
</button>
</div>
{error && <div className="refiner-error">{error}</div>}
<div className="chat-footer-actions">
{questionCount >= 2 && (
<button className="btn btn-secondary btn-sm" onClick={handleDoneEarly}>
I'm done - generate spec
</button>
)}
</div>
</div>
)}
{/* REVIEW PHASE */}
{phase === PHASES.REVIEW && (
<div className="refiner-review-phase">
<h2>Your Refined Game Spec</h2>
<p className="refiner-subtitle">Edit anything you'd like, then generate!</p>
<textarea
className="spec-editor"
value={refinedSpec}
onChange={(e) => setRefinedSpec(e.target.value)}
rows={12}
/>
{error && <div className="refiner-error">{error}</div>}
<div className="refiner-actions">
<button
className="btn btn-primary btn-lg"
onClick={() => onComplete(refinedSpec)}
disabled={!refinedSpec.trim()}
>
Use This Spec
</button>
<button
className="btn btn-secondary"
onClick={() => {
setPhase(PHASES.CHAT);
setRefinedSpec('');
}}
>
Ask more
</button>
<button
className="btn btn-secondary"
onClick={() => {
setPhase(PHASES.IDEA);
setConversation([]);
setRefinedSpec('');
setQuestionCount(0);
}}
>
Start over
</button>
</div>
</div>
)}
</div>
);
}
+361
View File
@@ -0,0 +1,361 @@
.share-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 1rem;
}
.share-modal {
background: white;
border-radius: 1rem;
padding: 2rem;
max-width: 500px;
width: 100%;
position: relative;
max-height: 90vh;
overflow-y: auto;
}
.share-close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
color: #94a3b8;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
}
.share-close:hover {
background: #f1f5f9;
color: #1e293b;
}
.share-modal h2 {
margin: 0 0 0.5rem;
color: #1e293b;
font-size: 1.5rem;
}
.share-game-title {
color: #6366f1;
font-weight: 500;
margin: 0 0 1.5rem;
}
.share-loading {
text-align: center;
padding: 2rem;
color: #64748b;
}
/* Tabs */
.share-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
background: #f1f5f9;
padding: 0.25rem;
border-radius: 0.5rem;
}
.share-tab {
flex: 1;
padding: 0.6rem 1rem;
border: none;
background: transparent;
border-radius: 0.375rem;
font-size: 0.9rem;
font-weight: 500;
color: #64748b;
cursor: pointer;
transition: all 0.2s;
}
.share-tab:hover {
color: #1e293b;
}
.share-tab.active {
background: white;
color: #6366f1;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Content */
.share-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.share-section label {
display: block;
font-size: 0.85rem;
font-weight: 500;
color: #475569;
margin-bottom: 0.5rem;
}
.share-input-group {
display: flex;
gap: 0.5rem;
}
.share-input-group input,
.share-input-group textarea {
flex: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
font-family: monospace;
font-size: 0.85rem;
color: #475569;
background: #f8fafc;
}
.share-input-group textarea {
resize: none;
}
.copy-btn {
padding: 0.75rem 1rem;
background: #6366f1;
color: white;
border: none;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.copy-btn:hover {
background: #4f46e5;
}
.copy-btn.copied {
background: #10b981;
}
/* Social Buttons */
.social-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.social-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border: none;
border-radius: 0.5rem;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
color: white;
}
.social-btn svg {
flex-shrink: 0;
}
.social-btn.twitter {
background: #000;
}
.social-btn.twitter:hover {
background: #333;
}
.social-btn.facebook {
background: #1877f2;
}
.social-btn.facebook:hover {
background: #0d6efd;
}
.social-btn.reddit {
background: #ff4500;
}
.social-btn.reddit:hover {
background: #e03d00;
}
.social-btn.whatsapp {
background: #25d366;
}
.social-btn.whatsapp:hover {
background: #1da851;
}
/* Embed Preview */
.embed-preview {
background: #1e293b;
border-radius: 0.5rem;
overflow: hidden;
}
.embed-preview iframe {
display: block;
border: none;
}
.embed-hint {
font-size: 0.8rem;
color: #94a3b8;
margin: 0.5rem 0 0;
}
/* Responsive */
@media (max-width: 480px) {
.share-modal {
padding: 1.5rem;
}
.social-buttons {
grid-template-columns: 1fr;
}
.share-input-group {
flex-direction: column;
}
.copy-btn {
width: 100%;
}
}
/* Touch-friendly styles */
@media (hover: none), (pointer: coarse) {
.share-close {
width: 44px;
height: 44px;
font-size: 1.75rem;
}
.share-tab {
min-height: 44px;
padding: 0.75rem 1rem;
}
.copy-btn {
min-height: 44px;
padding: 0.875rem 1.25rem;
}
.social-btn {
min-height: 48px;
padding: 0.875rem 1.25rem;
}
}
/* Active states for touch devices */
@media (hover: none) {
.share-close:active {
background: #e2e8f0;
transform: scale(0.95);
}
.share-tab:active {
background: #e2e8f0;
transform: scale(0.98);
}
.copy-btn:active {
background: #4338ca;
transform: scale(0.98);
}
.social-btn:active {
opacity: 0.8;
transform: scale(0.98);
}
.social-btn.twitter:active {
background: #1a1a1a;
}
.social-btn.facebook:active {
background: #0b5ed7;
}
.social-btn.reddit:active {
background: #cc3700;
}
.social-btn.whatsapp:active {
background: #128c48;
}
/* Disable iOS tap highlight */
.share-close,
.share-tab,
.copy-btn,
.social-btn {
-webkit-tap-highlight-color: transparent;
}
}
/* Hover-only styles for desktop */
@media (hover: hover) {
.share-close:hover {
background: #f1f5f9;
color: #1e293b;
}
.share-tab:hover {
color: #1e293b;
}
.copy-btn:hover {
background: #4f46e5;
}
.social-btn.twitter:hover {
background: #333;
}
.social-btn.facebook:hover {
background: #0d6efd;
}
.social-btn.reddit:hover {
background: #e03d00;
}
.social-btn.whatsapp:hover {
background: #1da851;
}
}
/* Focus states for accessibility */
.share-close:focus-visible,
.share-tab:focus-visible,
.copy-btn:focus-visible,
.social-btn:focus-visible {
outline: 3px solid #6366f1;
outline-offset: 2px;
}

Some files were not shown because too many files have changed in this diff Show More