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
+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