Initial commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -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
@@ -0,0 +1,966 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Avatar Creator - GamerComp</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg-dark: #1a1a2e;
|
||||
--bg-panel: #16213e;
|
||||
--bg-item: #0f3460;
|
||||
--accent: #e94560;
|
||||
--accent-hover: #ff6b6b;
|
||||
--text: #eee;
|
||||
--text-muted: #aaa;
|
||||
--border: #2a3a5e;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--bg-panel);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-item);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Preview Panel */
|
||||
.preview-panel {
|
||||
width: 300px;
|
||||
background: var(--bg-panel);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-canvas-container {
|
||||
background: linear-gradient(135deg, #2a3a5e, #1a2a4e);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#previewCanvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preview-modes {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-modes button {
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-item);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-modes button.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.preview-animations {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-animations button {
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.7rem;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-animations button:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Customization Panel */
|
||||
.customization-panel {
|
||||
flex: 1;
|
||||
background: var(--bg-panel);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
background: var(--bg-dark);
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.category-tab {
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.category-tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.category-tab.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.option-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.option-group h3 {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.option-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.option-grid.colors {
|
||||
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
|
||||
}
|
||||
|
||||
.option-item {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
background: var(--bg-item);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
border-color: var(--text-muted);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.option-item.selected {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 10px rgba(233, 69, 96, 0.4);
|
||||
}
|
||||
|
||||
.option-item.color-swatch {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.option-item img, .option-item canvas {
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
}
|
||||
|
||||
.option-item .label {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Accessories Panel */
|
||||
.accessories-panel {
|
||||
width: 280px;
|
||||
background: var(--bg-panel);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.accessories-panel h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.accessory-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.accessory-section h3 {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.accessory-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.accessory-item {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-item);
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.accessory-item:hover {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.accessory-item.equipped {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.accessory-item.locked {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.accessory-item .level-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 0.55rem;
|
||||
background: var(--bg-dark);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.rarity-common { border-color: #9d9d9d !important; }
|
||||
.rarity-uncommon { border-color: #1eff00 !important; }
|
||||
.rarity-rare { border-color: #0070dd !important; }
|
||||
.rarity-epic { border-color: #a335ee !important; }
|
||||
.rarity-legendary { border-color: #ff8000 !important; }
|
||||
.rarity-mythic { border-color: #e6cc80 !important; }
|
||||
|
||||
/* Randomize Button */
|
||||
.randomize-btn {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-item);
|
||||
border: 1px dashed var(--border);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.randomize-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 900px) {
|
||||
main {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-panel, .accessories-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-canvas-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Avatar Creator</h1>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-secondary" id="resetBtn">Reset</button>
|
||||
<button class="btn btn-primary" id="saveBtn">Save Avatar</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Preview Panel -->
|
||||
<div class="preview-panel">
|
||||
<div class="preview-canvas-container">
|
||||
<canvas id="previewCanvas" width="128" height="256"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="preview-modes">
|
||||
<button data-mode="HEAD_ONLY">Head</button>
|
||||
<button data-mode="HEAD_AND_SHOULDERS">Shoulders</button>
|
||||
<button data-mode="UPPER_BODY">Upper</button>
|
||||
<button data-mode="FULL_BODY" class="active">Full</button>
|
||||
</div>
|
||||
|
||||
<div class="preview-animations">
|
||||
<button data-anim="idle">Idle</button>
|
||||
<button data-anim="walk">Walk</button>
|
||||
<button data-anim="run">Run</button>
|
||||
<button data-anim="jump">Jump</button>
|
||||
<button data-anim="wave">Wave</button>
|
||||
<button data-anim="celebrate">Celebrate</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customization Panel -->
|
||||
<div class="customization-panel">
|
||||
<div class="category-tabs">
|
||||
<button class="category-tab active" data-category="face">Face</button>
|
||||
<button class="category-tab" data-category="eyes">Eyes</button>
|
||||
<button class="category-tab" data-category="hair">Hair</button>
|
||||
<button class="category-tab" data-category="body">Body</button>
|
||||
<button class="category-tab" data-category="clothing">Clothing</button>
|
||||
</div>
|
||||
|
||||
<div class="options-container" id="optionsContainer">
|
||||
<!-- Options populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accessories Panel -->
|
||||
<div class="accessories-panel">
|
||||
<h2>Equipped Accessories</h2>
|
||||
|
||||
<div class="accessory-section">
|
||||
<h3>Eyewear</h3>
|
||||
<div class="accessory-grid" id="eyewearGrid">
|
||||
<!-- Populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accessory-section">
|
||||
<h3>Headwear</h3>
|
||||
<div class="accessory-grid" id="headwearGrid">
|
||||
<!-- Populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accessory-section">
|
||||
<h3>Effects</h3>
|
||||
<div class="accessory-grid" id="effectsGrid">
|
||||
<!-- Populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="randomize-btn" id="randomizeBtn">
|
||||
<span>🎲</span> Randomize
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Load Avatar System -->
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Current avatar state
|
||||
var avatar = null;
|
||||
var currentMode = 'FULL_BODY';
|
||||
var currentAnimation = 'idle';
|
||||
var animationFrame = 0;
|
||||
var lastFrameTime = 0;
|
||||
|
||||
// Canvas setup
|
||||
var canvas = document.getElementById('previewCanvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
// Create default avatar
|
||||
avatar = AvatarSystem.createDefault();
|
||||
|
||||
// For demo, give some earned accessories
|
||||
avatar.earned = {
|
||||
eyewear: ['sunglasses-classic', 'nerd-glasses', 'aviator'],
|
||||
headwear: ['baseball-cap', 'beanie', 'crown-gold'],
|
||||
effects: ['sparkle-trail']
|
||||
};
|
||||
avatar.level = 42;
|
||||
|
||||
// Setup UI
|
||||
setupCategoryTabs();
|
||||
setupModeButtons();
|
||||
setupAnimationButtons();
|
||||
setupAccessoriesPanel();
|
||||
renderOptions('face');
|
||||
|
||||
// Start render loop
|
||||
requestAnimationFrame(renderLoop);
|
||||
}
|
||||
|
||||
// Render loop
|
||||
function renderLoop(time) {
|
||||
// Update animation frame
|
||||
var anim = window.AvatarAnimations && AvatarAnimations.getAnimation(currentMode, currentAnimation);
|
||||
if (anim) {
|
||||
var frameDuration = anim.frameDuration || 100;
|
||||
if (time - lastFrameTime > frameDuration) {
|
||||
animationFrame = (animationFrame + 1) % anim.frameCount;
|
||||
lastFrameTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear and render
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
var modeSize = window.AvatarSystem ? AvatarSystem.getModeSize(currentMode) : { width: 64, height: 160 };
|
||||
var scale = Math.min(
|
||||
(canvas.width - 20) / modeSize.width,
|
||||
(canvas.height - 20) / modeSize.height
|
||||
);
|
||||
|
||||
var x = canvas.width / 2;
|
||||
var y = canvas.height / 2 + (modeSize.height * scale) / 4;
|
||||
|
||||
if (window.AvatarSystem) {
|
||||
AvatarSystem.render(avatar, currentMode, ctx, x, y, {
|
||||
scale: scale,
|
||||
animation: currentAnimation,
|
||||
frame: animationFrame,
|
||||
time: time / 1000
|
||||
});
|
||||
} else {
|
||||
// Fallback placeholder
|
||||
ctx.fillStyle = '#e94560';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y - 40, 30, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#16213e';
|
||||
ctx.fillRect(x - 20, y - 5, 40, 50);
|
||||
}
|
||||
|
||||
requestAnimationFrame(renderLoop);
|
||||
}
|
||||
|
||||
// Category tabs
|
||||
function setupCategoryTabs() {
|
||||
var tabs = document.querySelectorAll('.category-tab');
|
||||
tabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
tabs.forEach(function(t) { t.classList.remove('active'); });
|
||||
tab.classList.add('active');
|
||||
renderOptions(tab.dataset.category);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Mode buttons
|
||||
function setupModeButtons() {
|
||||
var buttons = document.querySelectorAll('.preview-modes button');
|
||||
buttons.forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
buttons.forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
currentMode = btn.dataset.mode;
|
||||
animationFrame = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Animation buttons
|
||||
function setupAnimationButtons() {
|
||||
var buttons = document.querySelectorAll('.preview-animations button');
|
||||
buttons.forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
currentAnimation = btn.dataset.anim;
|
||||
animationFrame = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Render customization options
|
||||
function renderOptions(category) {
|
||||
var container = document.getElementById('optionsContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!window.AvatarData) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted); padding: 2rem;">Loading options...</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
switch (category) {
|
||||
case 'face':
|
||||
renderFaceOptions(container);
|
||||
break;
|
||||
case 'eyes':
|
||||
renderEyeOptions(container);
|
||||
break;
|
||||
case 'hair':
|
||||
renderHairOptions(container);
|
||||
break;
|
||||
case 'body':
|
||||
renderBodyOptions(container);
|
||||
break;
|
||||
case 'clothing':
|
||||
renderClothingOptions(container);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function renderFaceOptions(container) {
|
||||
// Face Shape
|
||||
var group1 = createOptionGroup('Face Shape');
|
||||
var grid1 = createOptionGrid();
|
||||
AvatarData.FACE_SHAPES.forEach(function(shape) {
|
||||
var item = createOptionItem(shape.name, shape.id === avatar.base.faceShape);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.faceShape = shape.id;
|
||||
renderOptions('face');
|
||||
});
|
||||
grid1.appendChild(item);
|
||||
});
|
||||
group1.appendChild(grid1);
|
||||
container.appendChild(group1);
|
||||
|
||||
// Skin Tone
|
||||
var group2 = createOptionGroup('Skin Tone');
|
||||
var grid2 = createOptionGrid('colors');
|
||||
AvatarData.SKIN_TONES.forEach(function(tone) {
|
||||
var item = createColorSwatch(tone.hex, avatar.base.skinTone === tone.hex);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.skinTone = tone.hex;
|
||||
renderOptions('face');
|
||||
});
|
||||
grid2.appendChild(item);
|
||||
});
|
||||
group2.appendChild(grid2);
|
||||
container.appendChild(group2);
|
||||
|
||||
// Nose
|
||||
var group3 = createOptionGroup('Nose');
|
||||
var grid3 = createOptionGrid();
|
||||
AvatarData.NOSE_SHAPES.forEach(function(nose) {
|
||||
var item = createOptionItem(nose.name, nose.id === avatar.base.nose);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.nose = nose.id;
|
||||
renderOptions('face');
|
||||
});
|
||||
grid3.appendChild(item);
|
||||
});
|
||||
group3.appendChild(grid3);
|
||||
container.appendChild(group3);
|
||||
|
||||
// Mouth
|
||||
var group4 = createOptionGroup('Mouth');
|
||||
var grid4 = createOptionGrid();
|
||||
AvatarData.MOUTH_SHAPES.forEach(function(mouth) {
|
||||
var item = createOptionItem(mouth.name, mouth.id === avatar.base.mouth);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.mouth = mouth.id;
|
||||
renderOptions('face');
|
||||
});
|
||||
grid4.appendChild(item);
|
||||
});
|
||||
group4.appendChild(grid4);
|
||||
container.appendChild(group4);
|
||||
}
|
||||
|
||||
function renderEyeOptions(container) {
|
||||
// Eye Shape
|
||||
var group1 = createOptionGroup('Eye Shape');
|
||||
var grid1 = createOptionGrid();
|
||||
AvatarData.EYE_SHAPES.forEach(function(shape) {
|
||||
var item = createOptionItem(shape.name, shape.id === avatar.base.eyes.shape);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.eyes.shape = shape.id;
|
||||
renderOptions('eyes');
|
||||
});
|
||||
grid1.appendChild(item);
|
||||
});
|
||||
group1.appendChild(grid1);
|
||||
container.appendChild(group1);
|
||||
|
||||
// Eye Color
|
||||
var group2 = createOptionGroup('Eye Color');
|
||||
var grid2 = createOptionGrid('colors');
|
||||
AvatarData.EYE_COLORS.forEach(function(color) {
|
||||
var item = createColorSwatch(color.hex, color.id === avatar.base.eyes.color);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.eyes.color = color.id;
|
||||
renderOptions('eyes');
|
||||
});
|
||||
grid2.appendChild(item);
|
||||
});
|
||||
group2.appendChild(grid2);
|
||||
container.appendChild(group2);
|
||||
|
||||
// Eyebrows
|
||||
var group3 = createOptionGroup('Eyebrows');
|
||||
var grid3 = createOptionGrid();
|
||||
AvatarData.EYEBROW_SHAPES.forEach(function(brow) {
|
||||
var item = createOptionItem(brow.name, brow.id === avatar.base.eyebrows);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.eyebrows = brow.id;
|
||||
renderOptions('eyes');
|
||||
});
|
||||
grid3.appendChild(item);
|
||||
});
|
||||
group3.appendChild(grid3);
|
||||
container.appendChild(group3);
|
||||
}
|
||||
|
||||
function renderHairOptions(container) {
|
||||
// Hair Style
|
||||
var group1 = createOptionGroup('Hair Style');
|
||||
var grid1 = createOptionGrid();
|
||||
AvatarData.HAIR_STYLES.forEach(function(style) {
|
||||
var item = createOptionItem(style.name, style.id === avatar.base.hair.style);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.hair.style = style.id;
|
||||
renderOptions('hair');
|
||||
});
|
||||
grid1.appendChild(item);
|
||||
});
|
||||
group1.appendChild(grid1);
|
||||
container.appendChild(group1);
|
||||
|
||||
// Hair Color
|
||||
var group2 = createOptionGroup('Hair Color');
|
||||
var grid2 = createOptionGrid('colors');
|
||||
AvatarData.HAIR_COLORS.forEach(function(color) {
|
||||
var item = createColorSwatch(color.hex, avatar.base.hair.color === color.hex);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.hair.color = color.hex;
|
||||
renderOptions('hair');
|
||||
});
|
||||
grid2.appendChild(item);
|
||||
});
|
||||
group2.appendChild(grid2);
|
||||
container.appendChild(group2);
|
||||
|
||||
// Facial Hair
|
||||
var group3 = createOptionGroup('Facial Hair');
|
||||
var grid3 = createOptionGrid();
|
||||
AvatarData.FACIAL_HAIR.forEach(function(fh) {
|
||||
var item = createOptionItem(fh.name, fh.id === avatar.base.facialHair);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.facialHair = fh.id;
|
||||
renderOptions('hair');
|
||||
});
|
||||
grid3.appendChild(item);
|
||||
});
|
||||
group3.appendChild(grid3);
|
||||
container.appendChild(group3);
|
||||
}
|
||||
|
||||
function renderBodyOptions(container) {
|
||||
// Body Type
|
||||
var group1 = createOptionGroup('Body Type');
|
||||
var grid1 = createOptionGrid();
|
||||
AvatarData.BODY_TYPES.forEach(function(type) {
|
||||
var item = createOptionItem(type.name, type.id === avatar.base.body.type);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.body.type = type.id;
|
||||
renderOptions('body');
|
||||
});
|
||||
grid1.appendChild(item);
|
||||
});
|
||||
group1.appendChild(grid1);
|
||||
container.appendChild(group1);
|
||||
}
|
||||
|
||||
function renderClothingOptions(container) {
|
||||
// Clothing Style
|
||||
var group1 = createOptionGroup('Style');
|
||||
var grid1 = createOptionGrid();
|
||||
AvatarData.CLOTHING_STYLES.forEach(function(style) {
|
||||
var item = createOptionItem(style.name, style.id === avatar.base.clothing.style);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.clothing.style = style.id;
|
||||
renderOptions('clothing');
|
||||
});
|
||||
grid1.appendChild(item);
|
||||
});
|
||||
group1.appendChild(grid1);
|
||||
container.appendChild(group1);
|
||||
|
||||
// Shirt Color
|
||||
var group2 = createOptionGroup('Shirt Color');
|
||||
var grid2 = createOptionGrid('colors');
|
||||
AvatarData.CLOTHING_COLORS.forEach(function(color) {
|
||||
var item = createColorSwatch(color, avatar.base.clothing.shirtColor === color);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.clothing.shirtColor = color;
|
||||
renderOptions('clothing');
|
||||
});
|
||||
grid2.appendChild(item);
|
||||
});
|
||||
group2.appendChild(grid2);
|
||||
container.appendChild(group2);
|
||||
|
||||
// Pants Color
|
||||
var group3 = createOptionGroup('Pants Color');
|
||||
var grid3 = createOptionGrid('colors');
|
||||
AvatarData.CLOTHING_COLORS.forEach(function(color) {
|
||||
var item = createColorSwatch(color, avatar.base.clothing.pantsColor === color);
|
||||
item.addEventListener('click', function() {
|
||||
avatar.base.clothing.pantsColor = color;
|
||||
renderOptions('clothing');
|
||||
});
|
||||
grid3.appendChild(item);
|
||||
});
|
||||
group3.appendChild(grid3);
|
||||
container.appendChild(group3);
|
||||
}
|
||||
|
||||
// Accessories panel
|
||||
function setupAccessoriesPanel() {
|
||||
updateAccessoriesUI();
|
||||
|
||||
document.getElementById('randomizeBtn').addEventListener('click', randomizeAvatar);
|
||||
document.getElementById('resetBtn').addEventListener('click', function() {
|
||||
avatar = AvatarSystem.createDefault();
|
||||
avatar.earned = { eyewear: ['sunglasses-classic'], headwear: ['baseball-cap'], effects: [] };
|
||||
avatar.level = 1;
|
||||
renderOptions('face');
|
||||
updateAccessoriesUI();
|
||||
});
|
||||
|
||||
document.getElementById('saveBtn').addEventListener('click', function() {
|
||||
// Would save to API
|
||||
console.log('Saving avatar:', JSON.stringify(avatar, null, 2));
|
||||
alert('Avatar saved! (Check console for data)');
|
||||
});
|
||||
}
|
||||
|
||||
function updateAccessoriesUI() {
|
||||
if (!window.AvatarAccessories) return;
|
||||
|
||||
// Eyewear
|
||||
var eyewearGrid = document.getElementById('eyewearGrid');
|
||||
eyewearGrid.innerHTML = '';
|
||||
var eyewearItems = AvatarAccessories.getByCategory('eyewear').slice(0, 8);
|
||||
eyewearItems.forEach(function(item) {
|
||||
var el = createAccessoryItem(item, 'eyewear');
|
||||
eyewearGrid.appendChild(el);
|
||||
});
|
||||
|
||||
// Headwear
|
||||
var headwearGrid = document.getElementById('headwearGrid');
|
||||
headwearGrid.innerHTML = '';
|
||||
var headwearItems = AvatarAccessories.getByCategory('headwear').slice(0, 8);
|
||||
headwearItems.forEach(function(item) {
|
||||
var el = createAccessoryItem(item, 'headwear');
|
||||
headwearGrid.appendChild(el);
|
||||
});
|
||||
|
||||
// Effects
|
||||
var effectsGrid = document.getElementById('effectsGrid');
|
||||
effectsGrid.innerHTML = '';
|
||||
var effectItems = AvatarAccessories.getByCategory('effect').slice(0, 8);
|
||||
effectItems.forEach(function(item) {
|
||||
var el = createAccessoryItem(item, 'effect');
|
||||
effectsGrid.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function createAccessoryItem(item, category) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'accessory-item';
|
||||
|
||||
var earned = avatar.earned[category === 'effect' ? 'effects' : category] || [];
|
||||
var isEarned = earned.includes(item.id);
|
||||
var isEquipped = avatar.equipped[category] === item.id;
|
||||
|
||||
if (!isEarned) {
|
||||
div.classList.add('locked');
|
||||
div.innerHTML = '<span class="level-badge">Lv' + item.unlockLevel + '</span>';
|
||||
} else {
|
||||
div.classList.add('rarity-' + item.rarity);
|
||||
if (isEquipped) div.classList.add('equipped');
|
||||
div.innerHTML = '<span style="font-size:1.2rem">' + getAccessoryEmoji(item.id) + '</span>';
|
||||
|
||||
div.addEventListener('click', function() {
|
||||
if (isEquipped) {
|
||||
avatar.equipped[category] = null;
|
||||
} else {
|
||||
avatar.equipped[category] = item.id;
|
||||
}
|
||||
updateAccessoriesUI();
|
||||
});
|
||||
}
|
||||
|
||||
div.title = item.name;
|
||||
return div;
|
||||
}
|
||||
|
||||
function getAccessoryEmoji(id) {
|
||||
var emojis = {
|
||||
'sunglasses-classic': '🕶️', 'aviator': '🥽', 'nerd-glasses': '🤓',
|
||||
'baseball-cap': '🧢', 'beanie': '🎿', 'crown-gold': '👑',
|
||||
'sparkle-trail': '✨', 'fire-aura': '🔥', 'lightning-crackle': '⚡'
|
||||
};
|
||||
return emojis[id] || '🎁';
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function createOptionGroup(title) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'option-group';
|
||||
var h3 = document.createElement('h3');
|
||||
h3.textContent = title;
|
||||
div.appendChild(h3);
|
||||
return div;
|
||||
}
|
||||
|
||||
function createOptionGrid(type) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'option-grid' + (type ? ' ' + type : '');
|
||||
return div;
|
||||
}
|
||||
|
||||
function createOptionItem(label, selected) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'option-item' + (selected ? ' selected' : '');
|
||||
var span = document.createElement('span');
|
||||
span.className = 'label';
|
||||
span.textContent = label.length > 8 ? label.substring(0, 7) + '…' : label;
|
||||
div.appendChild(span);
|
||||
return div;
|
||||
}
|
||||
|
||||
function createColorSwatch(color, selected) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'option-item color-swatch' + (selected ? ' selected' : '');
|
||||
div.style.backgroundColor = color;
|
||||
return div;
|
||||
}
|
||||
|
||||
function randomizeAvatar() {
|
||||
if (!window.AvatarData) return;
|
||||
|
||||
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
|
||||
|
||||
avatar.base.faceShape = pick(AvatarData.FACE_SHAPES).id;
|
||||
avatar.base.skinTone = pick(AvatarData.SKIN_TONES).hex;
|
||||
avatar.base.eyes.shape = pick(AvatarData.EYE_SHAPES).id;
|
||||
avatar.base.eyes.color = pick(AvatarData.EYE_COLORS).id;
|
||||
avatar.base.eyebrows = pick(AvatarData.EYEBROW_SHAPES).id;
|
||||
avatar.base.nose = pick(AvatarData.NOSE_SHAPES).id;
|
||||
avatar.base.mouth = pick(AvatarData.MOUTH_SHAPES).id;
|
||||
avatar.base.hair.style = pick(AvatarData.HAIR_STYLES).id;
|
||||
avatar.base.hair.color = pick(AvatarData.HAIR_COLORS).hex;
|
||||
avatar.base.facialHair = pick(AvatarData.FACIAL_HAIR).id;
|
||||
avatar.base.body.type = pick(AvatarData.BODY_TYPES).id;
|
||||
avatar.base.clothing.style = pick(AvatarData.CLOTHING_STYLES).id;
|
||||
avatar.base.clothing.shirtColor = pick(AvatarData.CLOTHING_COLORS);
|
||||
avatar.base.clothing.pantsColor = pick(AvatarData.CLOTHING_COLORS);
|
||||
|
||||
renderOptions(document.querySelector('.category-tab.active').dataset.category);
|
||||
}
|
||||
|
||||
// Initialize when ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Clear Cache - GamerComp</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
max-width: 450px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 { margin: 0 0 16px; font-size: 24px; }
|
||||
p { margin: 0 0 20px; opacity: 0.8; line-height: 1.5; }
|
||||
button {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 16px 32px;
|
||||
border-radius: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
button:hover { opacity: 0.9; }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-secondary {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
.status {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.success { background: rgba(34, 197, 94, 0.3); }
|
||||
.error { background: rgba(239, 68, 68, 0.3); }
|
||||
a { color: #a5b4fc; }
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
margin: 24px 0;
|
||||
}
|
||||
.manual-section {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.manual-section h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.shortcut {
|
||||
display: inline-block;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
margin: 2px;
|
||||
}
|
||||
.steps {
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.steps li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔄 Clear Cache</h1>
|
||||
<p>Having issues seeing updates? Clear all cached data for GamerComp.</p>
|
||||
|
||||
<button onclick="clearAllCaches()" id="clearBtn">Clear All Caches</button>
|
||||
<button onclick="forceReload()" class="btn-secondary" id="reloadBtn">Force Hard Reload</button>
|
||||
|
||||
<div id="status" class="status" style="display:none;"></div>
|
||||
|
||||
<div class="manual-section">
|
||||
<h3>🔧 Manual Hard Refresh</h3>
|
||||
<p style="font-size: 13px; margin-bottom: 12px;">If the buttons above don't work, try these keyboard shortcuts:</p>
|
||||
<p style="margin-bottom: 8px;">
|
||||
<strong>Windows/Linux:</strong><br>
|
||||
<span class="shortcut">Ctrl + Shift + R</span> or <span class="shortcut">Ctrl + F5</span>
|
||||
</p>
|
||||
<p style="margin-bottom: 0;">
|
||||
<strong>Mac:</strong><br>
|
||||
<span class="shortcut">Cmd + Shift + R</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<p style="font-size: 14px; margin-bottom: 0;">
|
||||
<a href="/">← Back to GamerComp</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function clearAllCaches() {
|
||||
const btn = document.getElementById('clearBtn');
|
||||
const status = document.getElementById('status');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Clearing...';
|
||||
status.style.display = 'block';
|
||||
status.className = 'status';
|
||||
status.textContent = 'Working...';
|
||||
|
||||
let results = [];
|
||||
|
||||
try {
|
||||
// 1. Unregister all service workers
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
for (const registration of registrations) {
|
||||
await registration.unregister();
|
||||
results.push('✓ Service worker unregistered');
|
||||
}
|
||||
if (registrations.length === 0) {
|
||||
results.push('✓ No service workers to remove');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Clear all caches (Cache API)
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
for (const cacheName of cacheNames) {
|
||||
await caches.delete(cacheName);
|
||||
results.push(`✓ Cache "${cacheName}" deleted`);
|
||||
}
|
||||
if (cacheNames.length === 0) {
|
||||
results.push('✓ No caches to clear');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clear localStorage (except auth token)
|
||||
const token = localStorage.getItem('token');
|
||||
localStorage.clear();
|
||||
if (token) {
|
||||
localStorage.setItem('token', token);
|
||||
results.push('✓ LocalStorage cleared (kept login)');
|
||||
} else {
|
||||
results.push('✓ LocalStorage cleared');
|
||||
}
|
||||
|
||||
// 4. Clear sessionStorage
|
||||
sessionStorage.clear();
|
||||
results.push('✓ SessionStorage cleared');
|
||||
|
||||
status.className = 'status success';
|
||||
status.innerHTML = `
|
||||
<strong>✅ All caches cleared!</strong><br><br>
|
||||
${results.join('<br>')}<br><br>
|
||||
<strong>Next step:</strong> Click "Force Hard Reload" below or press <span class="shortcut">Ctrl+Shift+R</span> to reload with fresh files.
|
||||
`;
|
||||
|
||||
btn.textContent = 'Done!';
|
||||
|
||||
} catch (err) {
|
||||
status.className = 'status error';
|
||||
status.textContent = 'Error: ' + err.message;
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Try Again';
|
||||
}
|
||||
}
|
||||
|
||||
function forceReload() {
|
||||
const status = document.getElementById('status');
|
||||
status.style.display = 'block';
|
||||
status.className = 'status';
|
||||
status.textContent = 'Reloading with fresh files...';
|
||||
|
||||
// Add cache-busting query param and reload
|
||||
// Using location.href with a timestamp forces a fresh fetch
|
||||
const freshUrl = '/?_=' + Date.now();
|
||||
|
||||
// Try to clear browser cache via fetch with cache: 'reload'
|
||||
// Then navigate to homepage
|
||||
fetch('/', { cache: 'reload' })
|
||||
.finally(() => {
|
||||
window.location.href = freshUrl;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,266 @@
|
||||
# Adding New Assets
|
||||
|
||||
Guide for extending the GamerComp asset library.
|
||||
|
||||
## Adding New Music
|
||||
|
||||
### 1. Define Song in musicLibrary.js
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'epic-21',
|
||||
name: 'Dawn of Heroes',
|
||||
mood: 'Epic',
|
||||
energy: 8,
|
||||
tempo: 130,
|
||||
key: 'G',
|
||||
scale: 'minor',
|
||||
tags: ['battle', 'adventure', 'cinematic'],
|
||||
desc: 'Triumphant orchestral piece for heroic moments',
|
||||
|
||||
// Instrument configuration
|
||||
synths: {
|
||||
melody: { type: 'AMSynth', options: { harmonicity: 2 } },
|
||||
bass: { type: 'FMSynth', options: { modulationIndex: 10 } },
|
||||
pad: { type: 'PolySynth', options: { voices: 4 } }
|
||||
},
|
||||
|
||||
// Effects chain
|
||||
fx: {
|
||||
reverb: { wet: 0.4, decay: 2 },
|
||||
delay: { wet: 0.2, time: '8n' }
|
||||
},
|
||||
|
||||
// Song sections with chord progressions
|
||||
sections: {
|
||||
intro: {
|
||||
chords: ['i', 'VI', 'III', 'VII'],
|
||||
duration: 8,
|
||||
instruments: ['pad']
|
||||
},
|
||||
verse: {
|
||||
chords: ['i', 'iv', 'VI', 'V'],
|
||||
duration: 16,
|
||||
instruments: ['melody', 'bass', 'pad']
|
||||
},
|
||||
chorus: {
|
||||
chords: ['VI', 'VII', 'i', 'i'],
|
||||
duration: 16,
|
||||
instruments: ['melody', 'bass', 'pad', 'drums']
|
||||
}
|
||||
},
|
||||
|
||||
// Section order
|
||||
form: ['intro', 'verse', 'chorus', 'verse', 'chorus', 'chorus']
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Update Catalog Stats
|
||||
|
||||
In `musicLibrary.js`, increment the count.
|
||||
|
||||
### 3. Test
|
||||
|
||||
```javascript
|
||||
MusicEngine.playSong('epic-21');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New Background Theme
|
||||
|
||||
### 1. Create Theme File
|
||||
|
||||
Create `themes/newtheme.js`:
|
||||
|
||||
```javascript
|
||||
(function(engine) {
|
||||
if (!engine) return;
|
||||
|
||||
engine.registerTheme('newtheme', {
|
||||
name: 'New Theme',
|
||||
description: 'Description of theme',
|
||||
|
||||
variants: {
|
||||
variant1: {
|
||||
meta: {
|
||||
name: 'Variant One',
|
||||
mood: 'wonder',
|
||||
tags: ['tag1', 'tag2'],
|
||||
description: 'Description',
|
||||
compatibleMusicMoods: ['Epic', 'Heroic'],
|
||||
recommendedFor: ['action', 'adventure']
|
||||
},
|
||||
colors: ['#000000', '#111111', '#222222', '#333333'],
|
||||
render: function(ctx, w, h, time, colors, opts) {
|
||||
// Background layer
|
||||
var gradient = ctx.createLinearGradient(0, 0, 0, h);
|
||||
gradient.addColorStop(0, colors[0]);
|
||||
gradient.addColorStop(1, colors[1]);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Animated elements
|
||||
// Use 'time' for animation (seconds)
|
||||
}
|
||||
},
|
||||
|
||||
// Add 7 more variants for 8 total
|
||||
}
|
||||
});
|
||||
|
||||
})(window.BackgroundEngine);
|
||||
```
|
||||
|
||||
### 2. Add Script Include
|
||||
|
||||
In templates, add:
|
||||
```html
|
||||
<script src="/assets/backgrounds/themes/newtheme.js"></script>
|
||||
```
|
||||
|
||||
### 3. Update Catalog
|
||||
|
||||
Add to `THEME_VARIANTS` in `backgroundCatalog.js`.
|
||||
|
||||
### 4. Test
|
||||
|
||||
```javascript
|
||||
BackgroundEngine.render(ctx, 'newtheme', 'variant1', w, h, 0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New Avatar Accessory
|
||||
|
||||
### 1. Add to accessories.js
|
||||
|
||||
```javascript
|
||||
// In EYEWEAR, HEADWEAR, or EFFECTS array:
|
||||
{
|
||||
id: 'new-item',
|
||||
name: 'New Item',
|
||||
category: 'headwear',
|
||||
unlockLevel: 50,
|
||||
rarity: 'epic',
|
||||
zIndex: 10,
|
||||
anchor: 'top',
|
||||
offset: { x: 0, y: -15 },
|
||||
scale: 1.0,
|
||||
hasEffect: false,
|
||||
description: 'Description of item'
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add Renderer in accessoryRenderer.js
|
||||
|
||||
```javascript
|
||||
// In appropriate RENDERERS object:
|
||||
'new-item': function(ctx, x, y, scale, color, time) {
|
||||
// Draw the accessory
|
||||
ctx.fillStyle = color || '#gold';
|
||||
ctx.beginPath();
|
||||
// ... drawing code
|
||||
ctx.fill();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Test
|
||||
|
||||
```javascript
|
||||
AvatarSystem.equipAccessory(avatar, 'headwear', 'new-item');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New Avatar Context
|
||||
|
||||
### 1. Add to Appropriate File
|
||||
|
||||
For vehicles, add to `contexts/vehicles.js`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'new-vehicle',
|
||||
name: 'New Vehicle',
|
||||
category: 'vehicle',
|
||||
avatarMode: 'HEAD_AND_SHOULDERS',
|
||||
animations: ['idle', 'hurt', 'celebrate'],
|
||||
defaultAnimation: 'idle',
|
||||
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
||||
layerOrder: ['background', 'avatar', 'foreground'],
|
||||
recommendedFor: ['action', 'racing'],
|
||||
description: 'Description',
|
||||
|
||||
avatarPosition: { x: 0.5, y: 0.4 },
|
||||
avatarScale: 1.0,
|
||||
|
||||
renderBackground: function(ctx, w, h, time, state) {
|
||||
// Draw background
|
||||
},
|
||||
|
||||
renderForeground: function(ctx, w, h, time, state) {
|
||||
// Draw foreground overlay
|
||||
},
|
||||
|
||||
contextEffects: {
|
||||
hurt: { type: 'shake', intensity: 5 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Update Catalog Mappings
|
||||
|
||||
In `contextCatalog.js`:
|
||||
- Add to `GAME_TYPE_CONTEXTS`
|
||||
- Will auto-aggregate via `getAll()`
|
||||
|
||||
### 3. Test
|
||||
|
||||
```javascript
|
||||
const awc = ContextRenderer.apply(avatar, 'new-vehicle');
|
||||
ContextRenderer.render(awc, ctx, x, y);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding New Preset
|
||||
|
||||
### 1. Add to presets.js
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'style-theme-##',
|
||||
gameStyle: 'action-arcade',
|
||||
name: 'Preset Name',
|
||||
description: 'What this combination provides',
|
||||
music: { mood: 'Epic', energy: 8 },
|
||||
background: { theme: 'space', variant: 'nebula' },
|
||||
avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' },
|
||||
colorPalette: 'space-blue',
|
||||
tags: ['tag1', 'tag2']
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Validate
|
||||
|
||||
```javascript
|
||||
const result = AssetCompatibility.validate(newPreset);
|
||||
console.log(result.score); // Should be > 70
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
1. **Visual Testing**: Use asset-browser.html to preview
|
||||
2. **Compatibility**: Run `AssetCompatibility.validate()`
|
||||
3. **Performance**: Test animation at 60fps
|
||||
4. **Mobile**: Test touch interactions
|
||||
5. **Integration**: Test in actual game template
|
||||
|
||||
## File Size Guidelines
|
||||
|
||||
- Keep individual JS files under 100KB
|
||||
- Music library can be larger (songs are data)
|
||||
- Optimize render functions (no complex math in loops)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,598 @@
|
||||
# GamerComp Asset Library
|
||||
|
||||
## Overview
|
||||
|
||||
The GamerComp Asset Library is a comprehensive procedural asset system providing:
|
||||
|
||||
- **160 Procedural Songs** - 8 moods x 20 songs each using Tone.js
|
||||
- **120 Procedural Backgrounds** - 15 themes x 8 variants using Canvas 2D
|
||||
- **Full Avatar System** - Mii-style customizable avatars with 45 accessories
|
||||
- **26 Game Contexts** - Place avatars in vehicles, costumes, or pure modes
|
||||
- **220 Presets** - Curated asset combinations for 11 game styles
|
||||
|
||||
All assets are procedurally generated - no external images or audio files needed.
|
||||
|
||||
---
|
||||
|
||||
## System Architecture
|
||||
|
||||
```
|
||||
+--------------------------------------------------------------------------------+
|
||||
| LAYER 4: ASSET MANAGER |
|
||||
| |
|
||||
| +----------------+ +----------------+ +-----------+ +----------------+ |
|
||||
| | assetManager.js| |assetCatalog.js | | presets.js| |compatibility.js| |
|
||||
| | (coordination) | |(unified query) | | (220) | | (validation) | |
|
||||
| +-------+--------+ +-------+--------+ +-----+-----+ +--------+-------+ |
|
||||
| | | | | |
|
||||
+-----------+-------------------+-----------------+-----------------+------------+
|
||||
| | | |
|
||||
v v v v
|
||||
+--------------------------------------------------------------------------------+
|
||||
| LAYER 3: CONTEXT SYSTEM |
|
||||
| |
|
||||
| +-------------+ +-------------+ +-------------+ +------------------+ |
|
||||
| | vehicles.js | | costumes.js | | pure.js | | contextRenderer | |
|
||||
| | (12) | | (10) | | (4) | | .js | |
|
||||
| +-------------+ +-------------+ +-------------+ +------------------+ |
|
||||
| |
|
||||
+--------------------------------------------------------------------------------+
|
||||
| | |
|
||||
v v v
|
||||
+--------------------------------------------------------------------------------+
|
||||
| LAYER 2: CONTENT LIBRARIES |
|
||||
| |
|
||||
| +------------------+ +------------------------+ +-----------------+ |
|
||||
| | musicLibrary.js | | 15 Background Theme | | avatarData.js | |
|
||||
| | (160 songs) | | Files (8 variants ea) | | accessories.js | |
|
||||
| +------------------+ +------------------------+ | (45 items) | |
|
||||
| | space.js nature.js | +-----------------+ |
|
||||
| | urban.js fantasy.js | |
|
||||
| | abstract.js retro.js | |
|
||||
| | indoor.js weather.js | |
|
||||
| | sports.js seasonal.js | |
|
||||
| | underwater.js sky.js | |
|
||||
| | mechanical.js spooky.js| |
|
||||
| | colorful.js | |
|
||||
| +------------------------+ |
|
||||
| |
|
||||
+--------------------------------------------------------------------------------+
|
||||
| | |
|
||||
v v v
|
||||
+--------------------------------------------------------------------------------+
|
||||
| LAYER 1: CORE ENGINES |
|
||||
| |
|
||||
| +------------------+ +--------------------+ +-------------------+ |
|
||||
| | MusicEngine | | BackgroundEngine | | AvatarSystem | |
|
||||
| | (Tone.js) | | (Canvas 2D) | | (Canvas 2D) | |
|
||||
| +------------------+ +--------------------+ +-------------------+ |
|
||||
| |
|
||||
+--------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Include Scripts
|
||||
|
||||
Scripts must be loaded in dependency order. Here is the recommended `<script>` order:
|
||||
|
||||
```html
|
||||
<!-- External dependency -->
|
||||
<script src="https://unpkg.com/tone@14.7.77/build/Tone.js"></script>
|
||||
|
||||
<!-- Layer 1: Core Engines -->
|
||||
<script src="/assets/music/musicEngine.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
|
||||
<!-- Layer 2: Content Libraries -->
|
||||
<script src="/assets/music/musicLibrary.js"></script>
|
||||
<script src="/assets/music/moodCategories.js"></script>
|
||||
<script src="/assets/backgrounds/themes/space.js"></script>
|
||||
<script src="/assets/backgrounds/themes/nature.js"></script>
|
||||
<script src="/assets/backgrounds/themes/urban.js"></script>
|
||||
<script src="/assets/backgrounds/themes/fantasy.js"></script>
|
||||
<script src="/assets/backgrounds/themes/abstract.js"></script>
|
||||
<script src="/assets/backgrounds/themes/retro.js"></script>
|
||||
<script src="/assets/backgrounds/themes/indoor.js"></script>
|
||||
<script src="/assets/backgrounds/themes/weather.js"></script>
|
||||
<script src="/assets/backgrounds/themes/sports.js"></script>
|
||||
<script src="/assets/backgrounds/themes/seasonal.js"></script>
|
||||
<script src="/assets/backgrounds/themes/underwater.js"></script>
|
||||
<script src="/assets/backgrounds/themes/sky.js"></script>
|
||||
<script src="/assets/backgrounds/themes/mechanical.js"></script>
|
||||
<script src="/assets/backgrounds/themes/spooky.js"></script>
|
||||
<script src="/assets/backgrounds/themes/colorful.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
|
||||
<!-- Layer 3: Context System -->
|
||||
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
||||
<script src="/assets/avatar/contexts/costumes.js"></script>
|
||||
<script src="/assets/avatar/contexts/pure.js"></script>
|
||||
<script src="/assets/avatar/contextCatalog.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<!-- Layer 4: Asset Manager -->
|
||||
<script src="/assets/assetCatalog.js"></script>
|
||||
<script src="/assets/compatibility.js"></script>
|
||||
<script src="/assets/presets.js"></script>
|
||||
<script src="/assets/assetManager.js"></script>
|
||||
```
|
||||
|
||||
### 2. Initialize
|
||||
|
||||
```javascript
|
||||
AssetManager.initialize().then(function(stats) {
|
||||
console.log('Asset Library initialized:', stats);
|
||||
// {
|
||||
// music: { songs: 160 },
|
||||
// backgrounds: { count: 120 },
|
||||
// avatarContexts: { count: 26 },
|
||||
// presets: { count: 220 }
|
||||
// }
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Get Recommendations
|
||||
|
||||
```javascript
|
||||
// Get AI-powered asset recommendations based on game description
|
||||
var recommendations = AssetManager.getRecommendations(
|
||||
'action-arcade', // Game style
|
||||
'space shooter with aliens', // Description/keywords
|
||||
'Epic' // Optional mood override
|
||||
);
|
||||
|
||||
// Returns top 3 recommendations with explanations
|
||||
// [
|
||||
// {
|
||||
// config: { music: {...}, background: {...}, avatarContext: {...} },
|
||||
// score: 85,
|
||||
// explanation: 'Epic high-energy music sets the mood; space nebula background...',
|
||||
// detectedTheme: 'space',
|
||||
// detectedMood: 'epic',
|
||||
// detectedMechanics: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
```
|
||||
|
||||
### 4. Load Assets
|
||||
|
||||
```javascript
|
||||
await AssetManager.loadGameAssets({
|
||||
music: { mood: 'Epic', energy: 8 },
|
||||
background: { theme: 'space', variant: 'nebula' },
|
||||
avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
|
||||
});
|
||||
|
||||
// Access loaded assets
|
||||
var assets = AssetManager.getLoadedAssets();
|
||||
// {
|
||||
// music: { id: 'epic_001', name: 'Siege of Thunder', ... },
|
||||
// background: { theme: 'space', variant: 'nebula', render: function(...) },
|
||||
// avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Zero External Dependencies** | Everything procedurally generated - no images, audio files, or CDN requirements |
|
||||
| **Consistent Style** | All assets designed to work together with unified color palettes and visual language |
|
||||
| **Intelligent Matching** | Compatibility system ensures good combinations with mood/theme/context scoring |
|
||||
| **Variety** | 220 presets provide instant diversity for any game style |
|
||||
| **Customizable** | Override any setting while keeping coherence through validation feedback |
|
||||
| **Lightweight** | All code compresses to ~150KB total (before gzip) |
|
||||
| **Offline-First** | Works without network after initial load |
|
||||
|
||||
---
|
||||
|
||||
## Asset Counts Summary
|
||||
|
||||
```
|
||||
+---------------------+--------+-----------------------------------------+
|
||||
| Category | Count | Details |
|
||||
+---------------------+--------+-----------------------------------------+
|
||||
| Music Songs | 160 | 8 moods x 20 songs each |
|
||||
| Background Variants | 120 | 15 themes x 8 variants each |
|
||||
| Avatar Accessories | 45 | Eyewear, headwear, effects, particles |
|
||||
| Game Contexts | 26 | 12 vehicles + 10 costumes + 4 pure |
|
||||
| Presets | 220 | 20 per game style x 11 game styles |
|
||||
| Game Styles | 11 | Action, Puzzle, Story, Racing, etc. |
|
||||
| Music Moods | 8 | Epic, Chill, Intense, Playful, etc. |
|
||||
| Background Themes | 15 | Space, Nature, Urban, Fantasy, etc. |
|
||||
| Color Palettes | 11 | Pre-defined harmonious color sets |
|
||||
+---------------------+--------+-----------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Music System
|
||||
|
||||
### Moods (8 total, 20 songs each)
|
||||
|
||||
| Mood | Energy Range | Description | Tags |
|
||||
|------|--------------|-------------|------|
|
||||
| **Epic** | 7-10 | Orchestral, triumphant, powerful | battle, hero, conquest |
|
||||
| **Chill** | 1-4 | Relaxed, ambient, peaceful | calm, zen, meditation |
|
||||
| **Intense** | 7-10 | Fast, aggressive, urgent | action, chase, combat |
|
||||
| **Playful** | 4-7 | Fun, bouncy, whimsical | happy, silly, cartoon |
|
||||
| **Mysterious** | 3-6 | Suspenseful, eerie, enigmatic | dark, secret, shadow |
|
||||
| **Heroic** | 6-9 | Triumphant, brave, noble | victory, champion, glory |
|
||||
| **Quirky** | 4-7 | Unusual, offbeat, eccentric | weird, wacky, unique |
|
||||
| **Ambient** | 1-4 | Atmospheric, floating, ethereal | space, dreamy, soft |
|
||||
|
||||
### Song Structure
|
||||
|
||||
Each song is defined with:
|
||||
- **id**: Unique identifier (e.g., `epic_001`)
|
||||
- **name**: Human-readable name
|
||||
- **mood**: One of the 8 mood categories
|
||||
- **energy**: 1-10 scale
|
||||
- **tempo**: BPM (60-180)
|
||||
- **key**: Musical key (C, D, E, etc.)
|
||||
- **scale**: Scale type (minor, major, dorian, phrygian, etc.)
|
||||
- **synths**: Lead, pad, and bass instrument types
|
||||
- **fx**: Reverb, delay, filter settings
|
||||
- **sections**: Intro, verse, chorus, bridge, outro with chord progressions
|
||||
- **form**: Section playback order
|
||||
|
||||
---
|
||||
|
||||
## Background System
|
||||
|
||||
### Themes (15 total, 8 variants each)
|
||||
|
||||
| Theme | Variants | Mood | Recommended For |
|
||||
|-------|----------|------|-----------------|
|
||||
| **space** | nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole | wonder, epic | Shooters, Sci-Fi |
|
||||
| **nature** | forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow | serene, peaceful | Adventure, Platformers |
|
||||
| **urban** | citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley | energetic, gritty | Racing, Action |
|
||||
| **fantasy** | castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower | magical, wonder | RPG, Adventure |
|
||||
| **abstract** | geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix | focused, trippy | Puzzle, Creative |
|
||||
| **retro** | pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave | nostalgic, fun | Arcade, Classic |
|
||||
| **indoor** | laboratory, classroom, bedroom, kitchen, arcade, office, library, gym | cozy, focused | Puzzle, Educational |
|
||||
| **weather** | rain, snow, storm, sunset, sunrise, fog, lightning, aurora | atmospheric, dynamic | Adventure, Mood |
|
||||
| **sports** | stadium, raceTrack, court, field, arena, pool, gym, skatepark | energetic, competitive | Sports, Racing |
|
||||
| **seasonal** | springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical | nostalgic, themed | Casual, Seasonal |
|
||||
| **underwater** | coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure | mysterious, serene | Adventure, Exploration |
|
||||
| **sky** | clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield | dreamy, free | Flying, Casual |
|
||||
| **mechanical** | gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab | industrial, complex | Puzzle, Tech |
|
||||
| **spooky** | hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows | eerie, tense | Horror-lite, Halloween |
|
||||
| **colorful** | rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism | vibrant, joyful | Kids, Casual |
|
||||
|
||||
---
|
||||
|
||||
## Avatar Context System
|
||||
|
||||
### Context Categories
|
||||
|
||||
#### Vehicles (12 contexts)
|
||||
Place your avatar inside vehicles with appropriate framing:
|
||||
|
||||
| Context | Avatar Mode | Description | Recommended For |
|
||||
|---------|-------------|-------------|-----------------|
|
||||
| spaceship-cockpit | HEAD_ONLY | Pilot view in spacecraft | Space shooters |
|
||||
| race-car | HEAD_AND_SHOULDERS | Racing car driver | Racing games |
|
||||
| airplane | HEAD_ONLY | Airplane pilot | Flying games |
|
||||
| flappy-style | HEAD_ONLY | Bird/flying creature | Flappy-type games |
|
||||
| tank | HEAD_ONLY | Tank commander | Action games |
|
||||
| pacman-style | FULL_BODY | Classic maze runner | Arcade games |
|
||||
| hoverboard | FULL_BODY | Futuristic board rider | Racing, Action |
|
||||
| jetpack | FULL_BODY | Jetpack flyer | Flying, Action |
|
||||
| mech-suit | HEAD_ONLY | Mech pilot | Action, Sci-Fi |
|
||||
| submarine | HEAD_AND_SHOULDERS | Sub commander | Underwater |
|
||||
| dragon-rider | FULL_BODY | Dragon mount | Fantasy, Adventure |
|
||||
| motorcycle | FULL_BODY | Motorcycle rider | Racing |
|
||||
|
||||
#### Costumes (10 contexts)
|
||||
Dress your avatar in themed outfits:
|
||||
|
||||
| Context | Avatar Mode | Description | Recommended For |
|
||||
|---------|-------------|-------------|-----------------|
|
||||
| knight-armor | FULL_BODY | Medieval knight | RPG, Adventure |
|
||||
| space-suit | FULL_BODY | Astronaut suit | Space, Sci-Fi |
|
||||
| ninja-outfit | FULL_BODY | Stealth ninja | Action, Stealth |
|
||||
| wizard-robes | FULL_BODY | Magic user | Fantasy, RPG |
|
||||
| superhero-cape | FULL_BODY | Superhero costume | Action |
|
||||
| athlete-uniform | FULL_BODY | Sports gear | Sports |
|
||||
| pirate-outfit | FULL_BODY | Pirate costume | Adventure |
|
||||
| scientist-labcoat | FULL_BODY | Lab scientist | Puzzle, Educational |
|
||||
| chef-outfit | FULL_BODY | Chef uniform | Cooking, Casual |
|
||||
| cowboy-gear | FULL_BODY | Western attire | Adventure |
|
||||
|
||||
#### Pure/Standard (4 contexts)
|
||||
Minimal modifications for classic gameplay:
|
||||
|
||||
| Context | Avatar Mode | Description | Recommended For |
|
||||
|---------|-------------|-------------|-----------------|
|
||||
| platformer-standard | FULL_BODY | Classic platformer | Platformers |
|
||||
| sports-player | FULL_BODY | Generic athlete | Sports |
|
||||
| adventure-hero | FULL_BODY | Generic adventurer | Adventure |
|
||||
| runner | FULL_BODY | Running character | Endless runners |
|
||||
|
||||
### Avatar Display Modes
|
||||
|
||||
```
|
||||
+----------------+ +-----------------------+ +-------------------+
|
||||
| HEAD_ONLY | | HEAD_AND_SHOULDERS | | FULL_BODY |
|
||||
| | | | | |
|
||||
| .---. | | .---. | | .---. |
|
||||
| ( o o ) | | ( o o ) | | ( o o ) |
|
||||
| '-.-' | | '-.-' | | '-.-' |
|
||||
| | | /| |\ | | /| |\ |
|
||||
| [Vehicle | | / | | \ | | / | | \ |
|
||||
| Cockpit] | | [Upper Body] | | / \ |
|
||||
| | | | | / \ |
|
||||
+----------------+ +-----------------------+ +-------------------+
|
||||
Used for: Used for: Used for:
|
||||
- Cockpits - Racing vehicles - Platformers
|
||||
- First-person - Seated contexts - Full character
|
||||
- Tight frames - Partial views - Action games
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Preset System
|
||||
|
||||
### Game Styles (11 total, 20 presets each)
|
||||
|
||||
| Style ID | Name | Description | Example Presets |
|
||||
|----------|------|-------------|-----------------|
|
||||
| action-arcade | Action/Arcade | Space shooters, platformers, fast action | Space Hero, Retro Runner, Tank Commander |
|
||||
| puzzle | Puzzle | Match-3, sliding, brain teasers | Zen Garden, Geometric Dreams, Candy Match |
|
||||
| story-adventure | Story Adventure | Visual novels, choose-your-adventure | Dragon's Quest, Enchanted Forest, Mystery Manor |
|
||||
| racing | Racing | Car racing, running, speed games | Speed Demon, Neon Racer, Desert Rally |
|
||||
| pet-simulator | Pet/Simulator | Virtual pets, farm games, life sim | Happy Farm, Pet Paradise, Garden Life |
|
||||
| trivia-quiz | Trivia/Quiz | Quiz shows, knowledge tests | Brain Challenge, Quiz Master, Fact Hunter |
|
||||
| music-rhythm | Music/Rhythm | Rhythm games, dance games | Disco Fever, Beat Drop, Neon Rhythm |
|
||||
| creative-drawing | Creative/Drawing | Drawing, design, art creation | Art Studio, Color Splash, Pixel Creator |
|
||||
| rpg-battle | RPG/Battle | Turn-based battles, dungeons | Dragon Slayer, Dark Dungeon, Hero's Quest |
|
||||
| sports | Sports | Ball games, athletics | Stadium Glory, Court King, Field Champion |
|
||||
| physics-pinball | Physics/Pinball | Pinball, physics puzzles | Pinball Wizard, Gravity Drop, Bounce Master |
|
||||
|
||||
### Preset Structure
|
||||
|
||||
Each preset defines a complete asset combination:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: "action-space-01",
|
||||
gameStyle: "action-arcade",
|
||||
name: "Space Hero",
|
||||
description: "Epic space battle with your avatar piloting a fighter",
|
||||
music: { mood: "Epic", energy: 9 },
|
||||
background: { theme: "space", variant: "nebula" },
|
||||
avatarContext: { mode: "HEAD_ONLY", context: "spaceship-cockpit" },
|
||||
colorPalette: "space-blue",
|
||||
tags: ["space", "shooter", "epic", "sci-fi"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compatibility System
|
||||
|
||||
### Compatibility Levels
|
||||
|
||||
The system validates combinations across four levels:
|
||||
|
||||
| Level | Description |
|
||||
|-------|-------------|
|
||||
| **Excellent** | Perfect match, highly recommended |
|
||||
| **Good** | Works well together |
|
||||
| **Neutral** | Acceptable, no issues |
|
||||
| **Poor** | May feel disjointed, suggests alternatives |
|
||||
|
||||
### Mood-Background Compatibility Matrix
|
||||
|
||||
```
|
||||
| space | nature | urban | fantasy | spooky | colorful | retro |
|
||||
-------------|-------|--------|-------|---------|--------|----------|-------|
|
||||
Epic | E | G | G | E | N | N | N |
|
||||
Chill | N | E | P | N | P | G | N |
|
||||
Intense | G | N | E | N | G | P | G |
|
||||
Playful | N | E | P | N | P | E | E |
|
||||
Mysterious | N | G | G | E | E | P | P |
|
||||
Heroic | E | G | E | E | N | P | N |
|
||||
Quirky | P | G | N | N | P | E | E |
|
||||
Ambient | N | E | P | N | P | G | N |
|
||||
|
||||
E = Excellent, G = Good, N = Neutral, P = Poor
|
||||
```
|
||||
|
||||
### Validation API
|
||||
|
||||
```javascript
|
||||
// Validate a combination
|
||||
var result = AssetCompatibility.validate({
|
||||
music: { mood: 'Epic' },
|
||||
background: { theme: 'space' },
|
||||
avatarContext: { context: 'spaceship-cockpit' }
|
||||
});
|
||||
|
||||
// {
|
||||
// valid: true,
|
||||
// score: 95,
|
||||
// issues: []
|
||||
// }
|
||||
|
||||
// Get suggestions for improvement
|
||||
var suggestions = AssetCompatibility.suggestImprovements(config);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Files
|
||||
|
||||
### Complete File Listing
|
||||
|
||||
```
|
||||
/frontend/public/assets/
|
||||
|
|
||||
+-- assetManager.js ~45 KB Main coordinator, recommendations
|
||||
+-- assetCatalog.js ~12 KB Unified queries, game styles
|
||||
+-- compatibility.js ~35 KB Validation rules, suggestions
|
||||
+-- presets.js ~65 KB 220 preset definitions
|
||||
|
|
||||
+-- music/
|
||||
| +-- musicEngine.js ~25 KB Tone.js synthesis engine
|
||||
| +-- musicLibrary.js ~80 KB 160 song definitions
|
||||
| +-- moodCategories.js ~5 KB Mood metadata
|
||||
|
|
||||
+-- backgrounds/
|
||||
| +-- backgroundEngine.js ~15 KB Canvas 2D render engine
|
||||
| +-- backgroundCatalog.js ~8 KB Catalog queries
|
||||
| +-- effects.js ~10 KB Shared visual effects
|
||||
| +-- themes/
|
||||
| +-- space.js ~12 KB 8 space variants
|
||||
| +-- nature.js ~15 KB 8 nature variants
|
||||
| +-- urban.js ~12 KB 8 urban variants
|
||||
| +-- fantasy.js ~14 KB 8 fantasy variants
|
||||
| +-- abstract.js ~10 KB 8 abstract variants
|
||||
| +-- retro.js ~11 KB 8 retro variants
|
||||
| +-- indoor.js ~13 KB 8 indoor variants
|
||||
| +-- weather.js ~12 KB 8 weather variants
|
||||
| +-- sports.js ~11 KB 8 sports variants
|
||||
| +-- seasonal.js ~12 KB 8 seasonal variants
|
||||
| +-- underwater.js ~13 KB 8 underwater variants
|
||||
| +-- sky.js ~10 KB 8 sky variants
|
||||
| +-- mechanical.js ~11 KB 8 mechanical variants
|
||||
| +-- spooky.js ~12 KB 8 spooky variants
|
||||
| +-- colorful.js ~10 KB 8 colorful variants
|
||||
|
|
||||
+-- avatar/
|
||||
+-- avatarSystem.js ~20 KB Avatar management
|
||||
+-- avatarRenderer.js ~25 KB Canvas 2D avatar drawing
|
||||
+-- avatarData.js ~15 KB Customization options
|
||||
+-- accessories.js ~18 KB 45 accessory definitions
|
||||
+-- animations.js ~12 KB Animation system
|
||||
+-- accessoryRenderer.js ~10 KB Accessory drawing
|
||||
+-- contextCatalog.js ~8 KB Context queries
|
||||
+-- contextRenderer.js ~10 KB Context rendering
|
||||
+-- contexts/
|
||||
+-- vehicles.js ~45 KB 12 vehicle contexts
|
||||
+-- costumes.js ~50 KB 10 costume contexts
|
||||
+-- pure.js ~15 KB 4 standard contexts
|
||||
|
||||
Total: ~650 KB (uncompressed JavaScript)
|
||||
~150 KB (gzipped)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Music Catalog](MUSIC_CATALOG.md) - Complete song listing and synthesis details
|
||||
- [Background Catalog](BACKGROUND_CATALOG.md) - All themes and variants
|
||||
- [Avatar System](AVATAR_SYSTEM.md) - Customization options and rendering
|
||||
- [Context Catalog](CONTEXT_CATALOG.md) - Vehicle, costume, and pure contexts
|
||||
- [API Reference](API_REFERENCE.md) - Complete API documentation
|
||||
- [Preset Guide](PRESET_GUIDE.md) - Using and creating presets
|
||||
- [Game Generation Guide](GAME_GENERATION_GUIDE.md) - Integrating assets into games
|
||||
- [Adding New Assets](ADDING_NEW_ASSETS.md) - Extending the library
|
||||
|
||||
---
|
||||
|
||||
## Interactive Tools
|
||||
|
||||
- **[Asset Browser](/asset-browser.html)** - Visual exploration of all assets, preview combinations
|
||||
- **[Avatar Creator](/avatar-creator/index.html)** - Test avatar customization, try contexts
|
||||
|
||||
---
|
||||
|
||||
## API Quick Reference
|
||||
|
||||
### AssetManager
|
||||
|
||||
```javascript
|
||||
// Initialization
|
||||
AssetManager.initialize(options) // Promise - Initialize all systems
|
||||
AssetManager.isInitialized() // boolean - Check ready state
|
||||
|
||||
// Recommendations
|
||||
AssetManager.getRecommendations( // Array - Get top 3 recommendations
|
||||
gameStyle, keywords, mood
|
||||
)
|
||||
|
||||
// Detection Helpers
|
||||
AssetManager.detectTheme(keywords) // string - Detect theme from keywords
|
||||
AssetManager.detectMood(keywords) // string - Detect mood from keywords
|
||||
AssetManager.detectMechanics(keywords) // Object - Detect avatar mode/context
|
||||
|
||||
// Preset Lookup
|
||||
AssetManager.findPreset(gameStyle, index) // Object - Find preset by style
|
||||
AssetManager.presetToConfig(preset) // Object - Convert to config
|
||||
|
||||
// Asset Loading
|
||||
AssetManager.loadGameAssets(config) // Promise - Load assets
|
||||
AssetManager.getLoadedAssets() // Object - Get current assets
|
||||
|
||||
// Validation
|
||||
AssetManager.validateCombination(config) // Object - Validate compatibility
|
||||
|
||||
// Utilities
|
||||
AssetManager.describeAssets(config) // string - Human description
|
||||
AssetManager.getVariation(config, type) // Object - Get variation
|
||||
AssetManager.searchAssets(query) // Object - Search all assets
|
||||
AssetManager.getRandomCombination(style) // Object - Random combo
|
||||
|
||||
// Direct Subsystem Access
|
||||
AssetManager.music() // MusicEngine
|
||||
AssetManager.backgrounds() // BackgroundEngine
|
||||
AssetManager.avatars() // AvatarSystem
|
||||
AssetManager.contexts() // ContextRenderer
|
||||
AssetManager.catalog() // AssetCatalog
|
||||
AssetManager.presets() // AssetPresets
|
||||
AssetManager.compatibility() // AssetCompatibility
|
||||
```
|
||||
|
||||
### AssetCompatibility
|
||||
|
||||
```javascript
|
||||
// Validation
|
||||
validate(config) // Object - Full validation
|
||||
checkFullCompatibility(mood, theme, ctx) // Object - Quick check
|
||||
|
||||
// Compatibility Queries
|
||||
getMoodBackgroundCompat(mood, theme) // string - Compatibility level
|
||||
getThemeContextCompat(theme, context) // string - Compatibility level
|
||||
getContextAnimationCompat(context, anim) // string - Animation compat
|
||||
getPaletteHarmony(palette1, palette2) // string - Color harmony
|
||||
|
||||
// Suggestions
|
||||
getMusicForTheme(theme) // Array - Compatible moods
|
||||
getBackgroundsForMood(mood) // Array - Compatible themes
|
||||
getContextsForTheme(theme) // Array - Compatible contexts
|
||||
getContextsForMechanics(mechanics) // Array - Mechanics-based
|
||||
suggestImprovements(config) // Array - Improvement suggestions
|
||||
|
||||
// Animation Helpers
|
||||
getRequiredAnimations(context) // Array - Required anims
|
||||
getRecommendedAnimations(context) // Array - Recommended anims
|
||||
getValidAnimations(context) // Object - All animation info
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0.0 | 2026-02-05 | Initial release with full asset library |
|
||||
|
||||
---
|
||||
|
||||
*GamerComp Asset Library - Procedural Game Assets for Everyone*
|
||||
@@ -0,0 +1,734 @@
|
||||
# Avatar System
|
||||
|
||||
## Overview
|
||||
|
||||
Mii-style avatar system with full customization and level-based unlockable accessories. The system supports four rendering modes optimized for different game types, with smooth animations and real-time accessory rendering.
|
||||
|
||||
## Architecture
|
||||
|
||||
The avatar system consists of several modules:
|
||||
|
||||
| Module | File | Purpose |
|
||||
|--------|------|---------|
|
||||
| AvatarData | `avatarData.js` | Customization options and guest avatars |
|
||||
| AvatarSystem | `avatarSystem.js` | Main API for creation, rendering, accessories |
|
||||
| AvatarRenderer | `avatarRenderer.js` | Canvas 2D rendering engine |
|
||||
| AvatarAnimations | `animations.js` | Animation definitions per mode |
|
||||
| AvatarAccessories | `accessories.js` | Unlockable accessories catalog |
|
||||
| AccessoryRenderer | `accessoryRenderer.js` | Accessory rendering on avatars |
|
||||
|
||||
---
|
||||
|
||||
## Customization Options
|
||||
|
||||
### Face Shape (8 options)
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| `round` | Round | Circular face with soft jawline |
|
||||
| `oval` | Oval | Classic elongated oval shape |
|
||||
| `square` | Square | Strong angular jawline |
|
||||
| `heart` | Heart | Wide forehead, pointed chin |
|
||||
| `long` | Long | Narrow, elongated face |
|
||||
| `wide` | Wide | Broader face shape |
|
||||
| `angular` | Angular | Defined cheekbones, sharp features |
|
||||
| `soft` | Soft | Rounded features throughout |
|
||||
|
||||
### Skin Tone (20 options)
|
||||
|
||||
| ID | Name | Hex Code |
|
||||
|----|------|----------|
|
||||
| `fair1` | Porcelain | `#FFECD1` |
|
||||
| `fair2` | Ivory | `#FFE4C4` |
|
||||
| `fair3` | Peach | `#FFDAB9` |
|
||||
| `fair4` | Cream | `#FFD5B5` |
|
||||
| `light1` | Light Beige | `#F5D0A9` |
|
||||
| `light2` | Warm Beige | `#E8C49A` |
|
||||
| `light3` | Sand | `#DEB887` |
|
||||
| `medium1` | Golden | `#D4A373` |
|
||||
| `medium2` | Honey | `#C9A06A` |
|
||||
| `medium3` | Caramel | `#BC8F5F` |
|
||||
| `medium4` | Toffee | `#B07D4B` |
|
||||
| `tan1` | Tan | `#A67B5B` |
|
||||
| `tan2` | Bronze | `#996B4D` |
|
||||
| `tan3` | Cinnamon | `#8B6D4C` |
|
||||
| `brown1` | Chestnut | `#7D5A3C` |
|
||||
| `brown2` | Cocoa | `#6F4E37` |
|
||||
| `brown3` | Espresso | `#5D4532` |
|
||||
| `dark1` | Mocha | `#4E3B2D` |
|
||||
| `dark2` | Ebony | `#3D2B1F` |
|
||||
| `dark3` | Onyx | `#2C1F15` |
|
||||
|
||||
### Eye Shape (12 options)
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| `round` | Round | Large, circular eyes |
|
||||
| `almond` | Almond | Classic almond shape |
|
||||
| `oval` | Oval | Soft oval shape |
|
||||
| `narrow` | Narrow | Slim, elongated eyes |
|
||||
| `wide` | Wide | Broad, expressive eyes |
|
||||
| `droopy` | Droopy | Downturned outer corners |
|
||||
| `upturned` | Upturned | Lifted outer corners |
|
||||
| `cat` | Cat | Sharp upturned cat-eye |
|
||||
| `sleepy` | Sleepy | Heavy-lidded, relaxed look |
|
||||
| `surprised` | Surprised | Wide open, alert |
|
||||
| `angry` | Angry | Angled inward brow line |
|
||||
| `happy` | Happy | Slightly squinted, joyful |
|
||||
|
||||
### Eye Color (16 options)
|
||||
|
||||
| ID | Name | Hex Code |
|
||||
|----|------|----------|
|
||||
| `brown` | Brown | `#634E34` |
|
||||
| `darkBrown` | Dark Brown | `#3D2314` |
|
||||
| `hazel` | Hazel | `#8B7355` |
|
||||
| `green` | Green | `#3D7A3D` |
|
||||
| `blue` | Blue | `#4682B4` |
|
||||
| `lightBlue` | Light Blue | `#87CEEB` |
|
||||
| `gray` | Gray | `#708090` |
|
||||
| `amber` | Amber | `#B8860B` |
|
||||
| `violet` | Violet | `#8B008B` |
|
||||
| `emerald` | Emerald | `#50C878` |
|
||||
| `iceBlue` | Ice Blue | `#A5D8E6` |
|
||||
| `gold` | Gold | `#DAA520` |
|
||||
| `red` | Red | `#8B0000` |
|
||||
| `black` | Black | `#1A1A1A` |
|
||||
| `honey` | Honey | `#C9A06A` |
|
||||
| `turquoise` | Turquoise | `#40E0D0` |
|
||||
|
||||
**Total Eye Combinations**: 12 shapes x 16 colors = **192 unique eye options**
|
||||
|
||||
### Eyebrow Shape (10 options)
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| `natural` | Natural | Balanced, standard arch |
|
||||
| `arched` | Arched | High dramatic arch |
|
||||
| `straight` | Straight | Flat, horizontal brows |
|
||||
| `thick` | Thick | Full, bold brows |
|
||||
| `thin` | Thin | Delicate, fine brows |
|
||||
| `angry` | Angry | Angled down toward center |
|
||||
| `worried` | Worried | Angled up toward center |
|
||||
| `bushy` | Bushy | Wild, untamed look |
|
||||
| `sleek` | Sleek | Thin with elegant arch |
|
||||
| `curved` | Curved | Pronounced curved shape |
|
||||
|
||||
### Nose Shape (8 options)
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| `button` | Button | Small, rounded tip |
|
||||
| `pointed` | Pointed | Narrow with defined tip |
|
||||
| `roman` | Roman | Prominent bridge curve |
|
||||
| `snub` | Snub | Short with slight upturn |
|
||||
| `wide` | Wide | Broader nostril base |
|
||||
| `narrow` | Narrow | Thin, elongated bridge |
|
||||
| `aquiline` | Aquiline | Pronounced curved bridge |
|
||||
| `flat` | Flat | Low bridge, wide base |
|
||||
|
||||
### Mouth Shape (10 options)
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| `smile` | Smile | Gentle upward curve |
|
||||
| `neutral` | Neutral | Relaxed, straight line |
|
||||
| `grin` | Grin | Wide smile showing teeth |
|
||||
| `smirk` | Smirk | Asymmetric half-smile |
|
||||
| `pout` | Pout | Full lips, slight downturn |
|
||||
| `open` | Open | Slightly parted lips |
|
||||
| `small` | Small | Petite, compact mouth |
|
||||
| `wide` | Wide | Broader mouth width |
|
||||
| `thin` | Thin | Narrow lip fullness |
|
||||
| `full` | Full | Plump, pronounced lips |
|
||||
|
||||
### Ear Shape (6 options)
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| `normal` | Normal | Standard ear shape and size |
|
||||
| `small` | Small | Compact, closer to head |
|
||||
| `large` | Large | Prominent, larger ears |
|
||||
| `pointed` | Pointed | Elf-like pointed tips |
|
||||
| `round` | Round | Soft, rounded lobes |
|
||||
| `flat` | Flat | Close-set against head |
|
||||
|
||||
### Hair Style (30 options)
|
||||
|
||||
| ID | Name | Length | Coverage |
|
||||
|----|------|--------|----------|
|
||||
| `short-messy` | Short Messy | Short | 0.30 |
|
||||
| `short-neat` | Short Neat | Short | 0.25 |
|
||||
| `short-curly` | Short Curly | Short | 0.35 |
|
||||
| `medium-straight` | Medium Straight | Medium | 0.50 |
|
||||
| `medium-wavy` | Medium Wavy | Medium | 0.55 |
|
||||
| `long-straight` | Long Straight | Long | 0.70 |
|
||||
| `long-wavy` | Long Wavy | Long | 0.75 |
|
||||
| `ponytail` | Ponytail | Medium | 0.40 |
|
||||
| `pigtails` | Pigtails | Medium | 0.45 |
|
||||
| `bun` | Bun | Long | 0.35 |
|
||||
| `mohawk` | Mohawk | Short | 0.20 |
|
||||
| `afro` | Afro | Medium | 0.80 |
|
||||
| `buzz` | Buzz Cut | Short | 0.15 |
|
||||
| `bald` | Bald | None | 0.00 |
|
||||
| `sidepart` | Side Part | Short | 0.30 |
|
||||
| `spiky` | Spiky | Short | 0.35 |
|
||||
| `slicked` | Slicked Back | Short | 0.25 |
|
||||
| `bob` | Bob | Medium | 0.50 |
|
||||
| `pixie` | Pixie | Short | 0.35 |
|
||||
| `braids` | Braids | Long | 0.60 |
|
||||
| `dreads` | Dreads | Long | 0.70 |
|
||||
| `undercut` | Undercut | Short | 0.25 |
|
||||
| `fauxhawk` | Faux Hawk | Short | 0.30 |
|
||||
| `mullet` | Mullet | Medium | 0.45 |
|
||||
| `bowl` | Bowl Cut | Medium | 0.55 |
|
||||
| `combover` | Comb Over | Short | 0.20 |
|
||||
| `bangs` | Bangs | Medium | 0.50 |
|
||||
| `sidesweep` | Side Sweep | Medium | 0.45 |
|
||||
| `topknot` | Top Knot | Long | 0.30 |
|
||||
| `cornrows` | Cornrows | Medium | 0.40 |
|
||||
|
||||
### Hair Color (20 options)
|
||||
|
||||
| ID | Name | Hex Code | Type |
|
||||
|----|------|----------|------|
|
||||
| `black` | Black | `#1A1A1A` | Natural |
|
||||
| `darkBrown` | Dark Brown | `#2C1810` | Natural |
|
||||
| `brown` | Brown | `#4A3728` | Natural |
|
||||
| `lightBrown` | Light Brown | `#7A5A3A` | Natural |
|
||||
| `auburn` | Auburn | `#8B4513` | Natural |
|
||||
| `ginger` | Ginger | `#B5651D` | Natural |
|
||||
| `strawberry` | Strawberry | `#C67171` | Natural |
|
||||
| `blonde` | Blonde | `#D4B896` | Natural |
|
||||
| `platinum` | Platinum | `#E8E4C9` | Natural |
|
||||
| `white` | White | `#F0F0F0` | Natural |
|
||||
| `gray` | Gray | `#808080` | Natural |
|
||||
| `red` | Red | `#CC0000` | Fantasy |
|
||||
| `blue` | Blue | `#0066CC` | Fantasy |
|
||||
| `purple` | Purple | `#7B2D8E` | Fantasy |
|
||||
| `green` | Green | `#228B22` | Fantasy |
|
||||
| `pink` | Pink | `#FF69B4` | Fantasy |
|
||||
| `teal` | Teal | `#008B8B` | Fantasy |
|
||||
| `orange` | Orange | `#FF6600` | Fantasy |
|
||||
| `silver` | Silver | `#C0C0C0` | Fantasy |
|
||||
| `rainbow` | Rainbow | Gradient | Fantasy |
|
||||
|
||||
### Facial Hair (10 options)
|
||||
|
||||
| ID | Name | Coverage | Description |
|
||||
|----|------|----------|-------------|
|
||||
| `none` | None | - | Clean shaven |
|
||||
| `stubble` | Stubble | Light | 5 o'clock shadow |
|
||||
| `goatee` | Goatee | Chin | Chin beard only |
|
||||
| `mustache` | Mustache | Upper lip | Classic mustache |
|
||||
| `fullBeard` | Full Beard | Full face | Complete beard coverage |
|
||||
| `shortBeard` | Short Beard | Full face | Trimmed full beard |
|
||||
| `soulPatch` | Soul Patch | Chin center | Small chin patch |
|
||||
| `mutton` | Mutton Chops | Sides | Sideburn-connected style |
|
||||
| `handlebar` | Handlebar | Upper lip | Curled mustache tips |
|
||||
| `vandyke` | Van Dyke | Chin + mustache | Goatee with mustache |
|
||||
|
||||
### Body Type (6 options)
|
||||
|
||||
| ID | Name | Width Ratio | Height Ratio |
|
||||
|----|------|-------------|--------------|
|
||||
| `slim` | Slim | 0.80 | 1.00 |
|
||||
| `average` | Average | 1.00 | 1.00 |
|
||||
| `athletic` | Athletic | 1.10 | 1.00 |
|
||||
| `stocky` | Stocky | 1.20 | 0.95 |
|
||||
| `tall` | Tall | 0.95 | 1.15 |
|
||||
| `short` | Short | 1.00 | 0.85 |
|
||||
|
||||
### Clothing Style (4 options)
|
||||
|
||||
| ID | Name | Has Collar | Sleeve Length | Special |
|
||||
|----|------|------------|---------------|---------|
|
||||
| `casual` | Casual | No | Short | - |
|
||||
| `sporty` | Sporty | No | Short | Stripes |
|
||||
| `formal` | Formal | Yes | Long | Buttons |
|
||||
| `scifi` | Sci-Fi | Yes | Long | Glow effects |
|
||||
|
||||
### Clothing Color (20 preset colors)
|
||||
|
||||
```
|
||||
#E74C3C #E67E22 #F1C40F #2ECC71 #1ABC9C
|
||||
#3498DB #9B59B6 #34495E #95A5A6 #ECF0F1
|
||||
#C0392B #D35400 #F39C12 #27AE60 #16A085
|
||||
#2980B9 #8E44AD #2C3E50 #7F8C8D #BDC3C7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Earned Accessories
|
||||
|
||||
### Rarity System
|
||||
|
||||
| Rarity | Color | Level Range | Hex Code |
|
||||
|--------|-------|-------------|----------|
|
||||
| Common | Gray | 1-15 | `#9D9D9D` |
|
||||
| Uncommon | Green | 16-30 | `#1EFF00` |
|
||||
| Rare | Blue | 31-50 | `#0070DD` |
|
||||
| Epic | Purple | 51-70 | `#A335EE` |
|
||||
| Legendary | Orange | 71-90 | `#FF8000` |
|
||||
| Mythic | Gold | 91-100 | `#E6CC80` |
|
||||
|
||||
### Eyewear (15 items)
|
||||
|
||||
| ID | Name | Unlock Level | Rarity | Has Effect | Description |
|
||||
|----|------|--------------|--------|------------|-------------|
|
||||
| `sunglasses-classic` | Classic Sunglasses | 5 | Common | No | Timeless shades for any occasion |
|
||||
| `aviator` | Aviator Glasses | 10 | Common | No | Top Gun approved eyewear |
|
||||
| `nerd-glasses` | Nerd Glasses | 15 | Common | No | Intelligence +100 (cosmetic only) |
|
||||
| `3d-glasses` | 3D Glasses | 20 | Uncommon | No | See the world in a whole new dimension |
|
||||
| `goggles` | Goggles | 25 | Uncommon | No | Ready for adventure or science experiments |
|
||||
| `monocle` | Monocle | 30 | Uncommon | No | Distinguished and sophisticated |
|
||||
| `star-glasses` | Star Glasses | 35 | Rare | No | You are a star! |
|
||||
| `heart-glasses` | Heart Glasses | 40 | Rare | No | Spread the love wherever you go |
|
||||
| `sport-visor` | Sport Visor | 45 | Rare | No | Professional gamer gear |
|
||||
| `vr-headset` | VR Headset | 50 | Epic | No | Immerse yourself in virtual worlds |
|
||||
| `cyber-visor` | Cyber Visor | 55 | Epic | No | Sleek futuristic eye protection |
|
||||
| `x-ray-specs` | X-Ray Specs | 60 | Epic | Yes | See through walls... probably |
|
||||
| `laser-eyes` | Laser Eyes | 70 | Legendary | Yes | Pew pew! Lasers shoot from your eyes |
|
||||
| `diamond-glasses` | Diamond Glasses | 80 | Legendary | No | Pure crystallized luxury |
|
||||
| `cosmic-lenses` | Cosmic Lenses | 90 | Mythic | Yes | Gaze upon the infinite universe |
|
||||
|
||||
### Headwear (20 items)
|
||||
|
||||
| ID | Name | Unlock Level | Rarity | Has Effect | Description |
|
||||
|----|------|--------------|--------|------------|-------------|
|
||||
| `baseball-cap` | Baseball Cap | 3 | Common | No | A classic casual cap |
|
||||
| `backwards-cap` | Backwards Cap | 8 | Common | No | Cool kids wear it backwards |
|
||||
| `beanie` | Beanie | 12 | Common | No | Cozy knitted headwear |
|
||||
| `party-hat` | Party Hat | 18 | Uncommon | No | Every day is a celebration! |
|
||||
| `chef-hat` | Chef Hat | 22 | Uncommon | No | Master of the kitchen |
|
||||
| `crown-bronze` | Bronze Crown | 25 | Uncommon | No | A humble crown for rising royalty |
|
||||
| `hard-hat` | Hard Hat | 26 | Uncommon | No | Safety first on the job site |
|
||||
| `wizard-hat` | Wizard Hat | 30 | Rare | No | Channel your inner sorcerer |
|
||||
| `pirate-hat` | Pirate Hat | 35 | Rare | No | Arrr! Set sail for adventure |
|
||||
| `santa-hat` | Santa Hat | 38 | Rare | No | Ho ho ho! Spread holiday cheer |
|
||||
| `crown-silver` | Silver Crown | 40 | Rare | No | A noble crown for dedicated players |
|
||||
| `graduation-cap` | Graduation Cap | 42 | Rare | No | Scholar of the gaming arts |
|
||||
| `bunny-ears` | Bunny Ears | 45 | Rare | No | Hop into the fun! |
|
||||
| `devil-horns` | Devil Horns | 48 | Rare | No | A little mischief never hurt anyone |
|
||||
| `halo` | Halo | 52 | Epic | Yes | An angelic golden ring of light |
|
||||
| `top-hat` | Top Hat | 56 | Epic | No | Dapper and distinguished |
|
||||
| `crown-gold` | Gold Crown | 60 | Epic | No | True royalty in golden splendor |
|
||||
| `viking-helmet` | Viking Helmet | 62 | Epic | No | Conquer games like a Norse warrior |
|
||||
| `crown-diamond` | Diamond Crown | 80 | Legendary | Yes | A magnificent crown of pure diamonds |
|
||||
| `crown-cosmic` | Cosmic Crown | 100 | Mythic | Yes | The ultimate crown, forged from stardust |
|
||||
|
||||
### Effects/Auras (10 items)
|
||||
|
||||
| ID | Name | Unlock Level | Rarity | Effect Type | Color | Description |
|
||||
|----|------|--------------|--------|-------------|-------|-------------|
|
||||
| `sparkle-trail` | Sparkle Trail | 15 | Uncommon | Particles | `#FFD700` | Leave a trail of golden sparkles |
|
||||
| `fire-aura` | Fire Aura | 25 | Rare | Glow | `#FF4500` | Surrounded by fierce flames |
|
||||
| `lightning-crackle` | Lightning Crackle | 35 | Rare | Electric | `#00BFFF` | Electricity crackles around you |
|
||||
| `rainbow-glow` | Rainbow Glow | 45 | Epic | Glow | Rainbow | Radiate all colors of the spectrum |
|
||||
| `star-particles` | Star Particles | 55 | Epic | Particles | `#FFFF00` | Tiny stars orbit around you |
|
||||
| `ice-crystals` | Ice Crystals | 65 | Legendary | Particles | `#87CEEB` | Frozen crystals shimmer around you |
|
||||
| `shadow-effect` | Shadow Effect | 70 | Legendary | Shadow | `#2F2F4F` | Darkness follows your every move |
|
||||
| `golden-glow` | Golden Glow | 75 | Legendary | Glow | `#FFD700` | Bathe in pure golden light |
|
||||
| `void-aura` | Void Aura | 85 | Mythic | Void | `#4B0082` | The void itself surrounds you |
|
||||
| `cosmic-trail` | Cosmic Trail | 95 | Mythic | Cosmic | Cosmic | Trail galaxies and nebulae in your wake |
|
||||
|
||||
**Total Accessories**: 15 + 20 + 10 = **45 unlockable items**
|
||||
|
||||
---
|
||||
|
||||
## Rendering Modes
|
||||
|
||||
### HEAD_ONLY (64x64 pixels)
|
||||
|
||||
**Best for**: Cockpit views, flying games (Flappy Bird style), maze games, puzzle games
|
||||
|
||||
**Shows**: Face, hair, ears, accessories (eyewear, headwear, effects)
|
||||
|
||||
**Animations** (5):
|
||||
| ID | Name | Frames | Duration | Loop | Description |
|
||||
|----|------|--------|----------|------|-------------|
|
||||
| `bob` | Bob | 3 | 150ms | Yes | Gentle up/down bobbing motion |
|
||||
| `flap` | Flap | 2 | 80ms | Yes | Flapping motion with head squash/stretch |
|
||||
| `blink` | Blink | 3 | 60ms | No | Eye blink animation |
|
||||
| `celebrate` | Celebrate | 4 | 100ms | No | Happy bouncing celebration |
|
||||
| `hurt` | Hurt | 2 | 80ms | No | Damage reaction shake |
|
||||
|
||||
### HEAD_AND_SHOULDERS (64x96 pixels)
|
||||
|
||||
**Best for**: Driving games, conversation scenes, submarine games, portrait views
|
||||
|
||||
**Shows**: Head, neck, shoulders, clothing collar
|
||||
|
||||
**Animations** (6):
|
||||
| ID | Name | Frames | Duration | Loop | Description |
|
||||
|----|------|--------|----------|------|-------------|
|
||||
| `idle` | Idle | 3 | 400ms | Yes | Subtle breathing motion |
|
||||
| `turn-left` | Turn Left | 2 | 100ms | No | Head turns to the left |
|
||||
| `turn-right` | Turn Right | 2 | 100ms | No | Head turns to the right |
|
||||
| `look-up` | Look Up | 2 | 100ms | No | Head tilts upward |
|
||||
| `celebrate` | Celebrate | 3 | 120ms | No | Bounce and smile celebration |
|
||||
| `hurt` | Hurt | 2 | 100ms | No | Recoil damage reaction |
|
||||
|
||||
### UPPER_BODY (64x128 pixels)
|
||||
|
||||
**Best for**: Racing games, shooting games, hoverboard games, action sequences
|
||||
|
||||
**Shows**: Head, neck, shoulders, torso, arms, hands
|
||||
|
||||
**Animations** (8):
|
||||
| ID | Name | Frames | Duration | Loop | Description |
|
||||
|----|------|--------|----------|------|-------------|
|
||||
| `idle` | Idle | 3 | 400ms | Yes | Breathing with subtle arm sway |
|
||||
| `wave` | Wave | 4 | 120ms | No | Friendly arm wave gesture |
|
||||
| `point` | Point | 3 | 100ms | No | Pointing forward gesture |
|
||||
| `steer-left` | Steer Left | 2 | 80ms | No | Leaning left for racing/steering |
|
||||
| `steer-right` | Steer Right | 2 | 80ms | No | Leaning right for racing/steering |
|
||||
| `shoot` | Shoot | 3 | 60ms | No | Arm extends forward shooting motion |
|
||||
| `celebrate` | Celebrate | 4 | 120ms | No | Arms up victory celebration |
|
||||
| `hurt` | Hurt | 2 | 100ms | No | Recoil backwards damage reaction |
|
||||
|
||||
### FULL_BODY (64x160 pixels)
|
||||
|
||||
**Best for**: Platformers, RPGs, sports games, adventure games, fighting games
|
||||
|
||||
**Shows**: Complete character with head, body, arms, legs, shoes
|
||||
|
||||
**Animations** (12):
|
||||
| ID | Name | Frames | Duration | Loop | Description |
|
||||
|----|------|--------|----------|------|-------------|
|
||||
| `idle` | Idle | 3 | 400ms | Yes | Standing with breathing motion |
|
||||
| `walk` | Walk Cycle | 4 | 100ms | Yes | Walking with arm/leg swing |
|
||||
| `run` | Run Cycle | 6 | 60ms | Yes | Running with full body movement |
|
||||
| `jump` | Jump | 3 | 100ms | No | Crouch, leap, stretch sequence |
|
||||
| `fall` | Fall | 2 | 150ms | Yes | Arms up, legs down falling pose |
|
||||
| `crouch` | Crouch | 2 | 80ms | No | Duck down into crouch position |
|
||||
| `celebrate` | Celebrate | 6 | 100ms | No | Jump and arm wave celebration |
|
||||
| `attack` | Attack | 4 | 60ms | No | Punch or swing attack motion |
|
||||
| `hurt` | Hurt | 3 | 80ms | No | Stagger backwards damage reaction |
|
||||
| `climb` | Climb | 4 | 120ms | Yes | Climbing ladder/wall motion |
|
||||
| `swim` | Swim | 4 | 150ms | Yes | Swimming stroke animation |
|
||||
| `fly` | Fly | 4 | 100ms | Yes | Superhero flying pose |
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Creating Avatars
|
||||
|
||||
```javascript
|
||||
// Create avatar with custom options
|
||||
const avatar = AvatarSystem.create({
|
||||
faceShape: 'round',
|
||||
skinTone: '#FFD5B8',
|
||||
eyes: { shape: 'round', color: '#4A90D9' },
|
||||
eyebrows: 'natural',
|
||||
nose: 'button',
|
||||
mouth: 'smile',
|
||||
ears: 'normal',
|
||||
hair: { style: 'short-messy', color: '#3D2314' },
|
||||
facialHair: 'none',
|
||||
body: { type: 'average' },
|
||||
clothing: { style: 'casual', shirtColor: '#3498DB', pantsColor: '#2C3E50' }
|
||||
});
|
||||
|
||||
// Create default avatar
|
||||
const defaultAvatar = AvatarSystem.createDefault();
|
||||
|
||||
// Get random guest avatar
|
||||
const guestAvatar = AvatarSystem.getGuestAvatar();
|
||||
```
|
||||
|
||||
### Loading and Saving
|
||||
|
||||
```javascript
|
||||
// Load user's saved avatar
|
||||
const avatar = await AvatarSystem.loadUserAvatar(userId);
|
||||
|
||||
// Save avatar
|
||||
AvatarSystem.saveAvatar(avatar);
|
||||
|
||||
// Clear cache
|
||||
AvatarSystem.clearCache(userId); // Clear specific user
|
||||
AvatarSystem.clearCache(); // Clear all
|
||||
```
|
||||
|
||||
### Rendering
|
||||
|
||||
```javascript
|
||||
// Render to canvas context
|
||||
AvatarSystem.render(avatar, 'FULL_BODY', ctx, x, y, {
|
||||
scale: 1,
|
||||
animation: 'walk',
|
||||
frame: 0
|
||||
});
|
||||
|
||||
// Render to offscreen canvas
|
||||
const canvas = AvatarSystem.renderToCanvas(avatar, 'HEAD_ONLY', { scale: 2 });
|
||||
|
||||
// Render to data URL (for images)
|
||||
const dataUrl = AvatarSystem.renderToDataURL(avatar, 'HEAD_AND_SHOULDERS');
|
||||
|
||||
// Get mode dimensions
|
||||
const size = AvatarSystem.getModeSize('FULL_BODY');
|
||||
// Returns: { width: 64, height: 160 }
|
||||
```
|
||||
|
||||
### Animation Control
|
||||
|
||||
```javascript
|
||||
// Start animation
|
||||
AvatarSystem.startAnimation(avatarId, 'walk', {
|
||||
loop: true,
|
||||
mode: 'FULL_BODY',
|
||||
onComplete: () => console.log('Animation finished')
|
||||
});
|
||||
|
||||
// Stop animation
|
||||
AvatarSystem.stopAnimation(avatarId);
|
||||
|
||||
// Get current animation state
|
||||
const state = AvatarSystem.getAnimationState(avatarId);
|
||||
// Returns: { animation: 'walk', frame: 2, complete: false, transforms: {...} }
|
||||
|
||||
// Check if animating
|
||||
const isAnimating = AvatarSystem.isAnimating(avatarId);
|
||||
|
||||
// Get animation frame transforms directly
|
||||
const transforms = AvatarSystem.animate(avatar, 'run', frameNumber);
|
||||
```
|
||||
|
||||
### Accessory Management
|
||||
|
||||
```javascript
|
||||
// Equip accessory
|
||||
AvatarSystem.equipAccessory(avatar, 'headwear', 'crown-gold');
|
||||
AvatarSystem.equipAccessory(avatar, 'eyewear', 'sunglasses-classic');
|
||||
AvatarSystem.equipAccessory(avatar, 'effect', 'sparkle-trail');
|
||||
|
||||
// Unequip accessory
|
||||
AvatarSystem.unequipAccessory(avatar, 'headwear');
|
||||
|
||||
// Unlock/award accessory
|
||||
const newlyUnlocked = AvatarSystem.unlockAccessory(avatar, 'vr-headset');
|
||||
// Returns: true if newly unlocked, false if already owned
|
||||
|
||||
// Check if avatar has accessory
|
||||
const hasItem = AvatarSystem.hasAccessory(avatar, 'crown-gold');
|
||||
|
||||
// Get earned accessories (full objects)
|
||||
const earned = AvatarSystem.getEarnedAccessories(avatar);
|
||||
// Returns: { eyewear: [...], headwear: [...], effects: [...] }
|
||||
|
||||
// Get equipped accessories (full objects)
|
||||
const equipped = AvatarSystem.getEquippedAccessories(avatar);
|
||||
// Returns: { eyewear: {...}, headwear: {...}, effect: {...} }
|
||||
|
||||
// Get full accessory catalog
|
||||
const catalog = AvatarSystem.getAccessoryCatalog();
|
||||
```
|
||||
|
||||
### Level Unlocks
|
||||
|
||||
```javascript
|
||||
// Check what unlocks at a specific level
|
||||
const newItems = AvatarSystem.checkLevelUnlocks(avatar, 50);
|
||||
// Returns: Array of newly unlocked accessory objects
|
||||
|
||||
// Add XP and check for level ups
|
||||
const result = AvatarSystem.addXP(avatar, 500);
|
||||
// Returns: { newLevel: 12, leveledUp: true, unlockedItems: [...] }
|
||||
|
||||
// Get XP progress info
|
||||
const xpInfo = AvatarSystem.getXPInfo(avatar);
|
||||
// Returns: { level, totalXP, currentLevelXP, xpNeededForLevel, progress }
|
||||
```
|
||||
|
||||
### Game Integration
|
||||
|
||||
```javascript
|
||||
// Export avatar for use in a game
|
||||
const gameAvatar = AvatarSystem.exportForGame(avatar, 'FULL_BODY');
|
||||
// Returns object with render() and animate() methods
|
||||
|
||||
// Use in game loop
|
||||
gameAvatar.render(ctx, x, y, { animation: 'run', frame: frameNum });
|
||||
const animInfo = gameAvatar.getAnimationInfo('run');
|
||||
|
||||
// Export minimal data for network transmission
|
||||
const minimal = AvatarSystem.exportMinimal(avatar);
|
||||
|
||||
// Import from minimal data
|
||||
const fullAvatar = AvatarSystem.importMinimal(minimalData, userId);
|
||||
|
||||
// Create sprite sheet for performance
|
||||
const spriteSheet = AvatarSystem.createSpriteSheet(avatar, 'FULL_BODY', ['idle', 'walk', 'run']);
|
||||
// Returns: { canvas, frameWidth, frameHeight, getFrameRect(animId, frame) }
|
||||
```
|
||||
|
||||
### Utility Functions
|
||||
|
||||
```javascript
|
||||
// Clone avatar
|
||||
const cloned = AvatarSystem.clone(avatar);
|
||||
|
||||
// Update base properties
|
||||
AvatarSystem.updateBase(avatar, { hair: { style: 'afro', color: '#000000' } });
|
||||
|
||||
// Validate avatar data
|
||||
const isValid = AvatarSystem.validate(avatarData);
|
||||
|
||||
// Compare avatars
|
||||
const areEqual = AvatarSystem.equals(avatar1, avatar2);
|
||||
|
||||
// Get all customization options
|
||||
const options = AvatarSystem.getCustomizationOptions();
|
||||
// Returns: { faceShapes, skinTones, eyeShapes, eyeColors, ... }
|
||||
|
||||
// Check if guest avatar
|
||||
const isGuest = AvatarSystem.isGuest(avatar);
|
||||
|
||||
// Convert guest to registered user
|
||||
const userAvatar = AvatarSystem.convertGuestToUser(guestAvatar, newUserId);
|
||||
|
||||
// Get version info
|
||||
const version = AvatarSystem.getVersion();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guest Avatars
|
||||
|
||||
10 pre-defined guest avatars are available for non-logged-in users. Each has a unique appearance:
|
||||
|
||||
| ID | Name | Skin Tone | Hair Style | Eye Color | Clothing Color |
|
||||
|----|------|-----------|------------|-----------|----------------|
|
||||
| `guest_1` | Guest Blue | Medium | Short Messy | Blue | `#3498DB` |
|
||||
| `guest_2` | Guest Red | Light | Long Straight | Brown | `#E74C3C` |
|
||||
| `guest_3` | Guest Green | Tan | Buzz Cut | Dark Brown | `#2ECC71` |
|
||||
| `guest_4` | Guest Purple | Fair | Bob | Green | `#9B59B6` |
|
||||
| `guest_5` | Guest Orange | Brown | Afro | Amber | `#E67E22` |
|
||||
| `guest_6` | Guest Teal | Dark | Dreads | Dark Brown | `#1ABC9C` |
|
||||
| `guest_7` | Guest Gray | Light | Undercut | Gray | `#95A5A6` |
|
||||
| `guest_8` | Guest Yellow | Medium | Spiky | Hazel | `#F1C40F` |
|
||||
| `guest_9` | Guest Navy | Tan | Comb Over | Brown | `#2C3E50` |
|
||||
| `guest_10` | Guest Pink | Fair | Pigtails | Light Blue | `#FF69B4` |
|
||||
|
||||
```javascript
|
||||
// Get random guest avatar
|
||||
const guest = AvatarSystem.getGuestAvatar();
|
||||
|
||||
// Or use AvatarData directly
|
||||
const guestData = AvatarData.getRandomGuestAvatar();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## XP and Leveling System
|
||||
|
||||
### XP Per Level Formula
|
||||
|
||||
```
|
||||
XP needed for level N = 100 + (N - 1) * 50
|
||||
```
|
||||
|
||||
| Level | XP Needed | Cumulative XP |
|
||||
|-------|-----------|---------------|
|
||||
| 1 | 100 | 100 |
|
||||
| 2 | 150 | 250 |
|
||||
| 3 | 200 | 450 |
|
||||
| 5 | 300 | 1,000 |
|
||||
| 10 | 550 | 3,500 |
|
||||
| 25 | 1,300 | 18,250 |
|
||||
| 50 | 2,550 | 68,000 |
|
||||
| 75 | 3,800 | 148,500 |
|
||||
| 100 | 5,050 | 259,750 |
|
||||
|
||||
### Unlock Schedule
|
||||
|
||||
All accessories unlock by reaching specific levels:
|
||||
|
||||
- **Levels 3-15**: 6 Common items
|
||||
- **Levels 18-30**: 8 Uncommon items
|
||||
- **Levels 35-48**: 10 Rare items
|
||||
- **Levels 50-62**: 8 Epic items
|
||||
- **Levels 65-80**: 6 Legendary items
|
||||
- **Levels 85-100**: 4 Mythic items
|
||||
|
||||
---
|
||||
|
||||
## Accessory Animation Rules
|
||||
|
||||
Accessories follow specific animation behavior:
|
||||
|
||||
```javascript
|
||||
{
|
||||
headwear: {
|
||||
followHead: true, // Moves with head
|
||||
bounceMultiplier: 1.2, // Extra bounce on jumps
|
||||
rotationDamping: 0.8 // Slightly less rotation than head
|
||||
},
|
||||
eyewear: {
|
||||
followHead: true, // Moves with head
|
||||
bounceMultiplier: 1.0, // Same bounce as head
|
||||
rotationDamping: 1.0 // Matches head rotation exactly
|
||||
},
|
||||
effects: {
|
||||
independent: true, // Own animation cycle
|
||||
particleSpeed: 1.0 // Particle emission rate
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Sprite Caching
|
||||
|
||||
For games with many avatars or frequent redraws:
|
||||
|
||||
```javascript
|
||||
// Pre-render to sprite cache
|
||||
const cache = AvatarRenderer.createSpriteCache(avatars, 'HEAD_ONLY', 1);
|
||||
|
||||
// Render from cache (much faster)
|
||||
AvatarRenderer.renderFromCache(ctx, cache, avatarId, x, y);
|
||||
```
|
||||
|
||||
### Sprite Sheets
|
||||
|
||||
For animation-heavy games:
|
||||
|
||||
```javascript
|
||||
const sheet = AvatarSystem.createSpriteSheet(avatar, 'FULL_BODY', ['idle', 'walk', 'run', 'jump']);
|
||||
|
||||
// In game loop
|
||||
const rect = sheet.getFrameRect('walk', currentFrame);
|
||||
ctx.drawImage(sheet.image, rect.x, rect.y, rect.width, rect.height, destX, destY, rect.width, rect.height);
|
||||
```
|
||||
|
||||
### Batch Rendering
|
||||
|
||||
For many avatars on screen:
|
||||
|
||||
```javascript
|
||||
AvatarRenderer.renderBatch(ctx, avatars, 'HEAD_ONLY', positions);
|
||||
// Automatically skips off-screen avatars
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Dependencies
|
||||
|
||||
Load order matters. Include scripts in this order:
|
||||
|
||||
```html
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
```
|
||||
|
||||
Or use the asset manager which handles loading automatically.
|
||||
@@ -0,0 +1,470 @@
|
||||
# Background Catalog
|
||||
|
||||
Complete catalog of all 120 procedural backgrounds available in the GamerComp game generation system.
|
||||
|
||||
**15 themes x 8 variants = 120 unique backgrounds**
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Theme Categories](#theme-categories)
|
||||
- [Space](#space-theme)
|
||||
- [Nature](#nature-theme)
|
||||
- [Urban](#urban-theme)
|
||||
- [Fantasy](#fantasy-theme)
|
||||
- [Abstract](#abstract-theme)
|
||||
- [Retro](#retro-theme)
|
||||
- [Indoor](#indoor-theme)
|
||||
- [Weather](#weather-theme)
|
||||
- [Sports](#sports-theme)
|
||||
- [Seasonal](#seasonal-theme)
|
||||
- [Underwater](#underwater-theme)
|
||||
- [Sky](#sky-theme)
|
||||
- [Mechanical](#mechanical-theme)
|
||||
- [Spooky](#spooky-theme)
|
||||
- [Colorful](#colorful-theme)
|
||||
3. [Compatible Music Moods](#compatible-music-moods)
|
||||
4. [Compatible Avatar Contexts](#compatible-avatar-contexts)
|
||||
5. [API Usage](#api-usage)
|
||||
6. [Visual Preview Guide](#visual-preview-guide)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
All backgrounds are procedurally generated using Canvas 2D rendering. Each background features:
|
||||
|
||||
- **Animated elements** - Smooth animations driven by time parameter
|
||||
- **Layered rendering** - Base layer, mid layer, and particle effects
|
||||
- **Customizable colors** - 4-color palette per background
|
||||
- **Particle systems** - Optional ambient particles (stars, rain, sparkles, etc.)
|
||||
- **Mood-based filtering** - Find backgrounds by emotional tone
|
||||
- **Game genre recommendations** - Suggested game types for each background
|
||||
|
||||
### Particle Types Available
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `stars` | Twinkling star field |
|
||||
| `rain` | Falling rain drops |
|
||||
| `snow` | Gentle snowfall |
|
||||
| `particles` | Generic floating particles |
|
||||
| `fog` | Drifting fog wisps |
|
||||
| `bubbles` | Rising bubbles |
|
||||
| `leaves` | Falling leaves |
|
||||
| `sparkles` | Glittering sparkles |
|
||||
| `fireflies` | Glowing fireflies |
|
||||
| `dust` | Floating dust motes |
|
||||
|
||||
---
|
||||
|
||||
## Theme Categories
|
||||
|
||||
### Space Theme
|
||||
|
||||
Cosmic environments featuring nebulas, planets, and interstellar phenomena.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Nebula | `space_nebula` | Swirling nebula clouds with twinkling stars | wonder | `#0B0B2B` `#1A0A3E` `#3D1A78` `#7B2FBE` | sparkles | puzzle, story, creative |
|
||||
| Asteroid Field | `space_asteroidField` | Rocky asteroids drifting through dark space | tense | `#0A0A14` `#1C1C2E` `#4A3A2A` `#8B7355` | dust | action, racing, sports |
|
||||
| Deep Space | `space_deepSpace` | The void of deep space with distant starlight | serene | `#000005` `#050510` `#0A0A20` `#111133` | stars | puzzle, story, rpg |
|
||||
| Planets | `space_planets` | Colorful planets against a starry backdrop | wonder | `#0B0B2B` `#0F1640` `#CC5533` `#44BBAA` | sparkles | creative, rpg, story |
|
||||
| Space Station | `space_spaceStation` | Orbital station with geometric structures | focused | `#0A0A1E` `#151530` `#3A3A55` `#88AACC` | dust | puzzle, action, physics |
|
||||
| Warp Speed | `space_warpSpeed` | Light streaks from faster-than-light travel | energetic | `#000010` `#0A0A30` `#2244AA` `#66BBFF` | particles | racing, action, sports |
|
||||
| Satellites | `space_satellites` | Satellites orbiting above a dark Earth | calm | `#060615` `#101030` `#334466` `#AACCDD` | stars | puzzle, trivia, creative |
|
||||
| Black Hole | `space_blackHole` | A black hole bending light around its event horizon | intense | `#000000` `#0A0510` `#220A33` `#FF6622` | particles | action, rpg, physics |
|
||||
|
||||
---
|
||||
|
||||
### Nature Theme
|
||||
|
||||
Natural outdoor environments from forests to oceans.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Forest | `nature_forest` | A dense forest with layered canopy and dappled light | peaceful | `#1A3320` `#2D5A27` `#4A7A3F` `#8FB86A` | fireflies | story, rpg, puzzle |
|
||||
| Jungle | `nature_jungle` | Thick tropical jungle with hanging vines and humid air | adventurous | `#0D2B0D` `#1A4D1A` `#2D7A2D` `#55AA33` | fireflies | action, rpg, pet |
|
||||
| Desert | `nature_desert` | Rolling sand dunes under a blazing sun | warm | `#F4A460` `#DAA06D` `#C2955A` `#8B6914` | dust | racing, action, rpg |
|
||||
| Mountains | `nature_mountains` | Snow-capped mountain peaks under sweeping skies | majestic | `#2C3E50` `#4A6B5A` `#7A9B6B` `#FFFFFF` | fog | rpg, sports, story |
|
||||
| Ocean | `nature_ocean` | Vast ocean stretching to the horizon at sunset | serene | `#1A5276` `#2E86C1` `#5DADE2` `#AED6F1` | fog | puzzle, story, creative |
|
||||
| Underwater | `nature_underwater` | Deep beneath the waves with shafts of sunlight | mystical | `#0A2342` `#1A4B6E` `#2E86C1` `#48D1CC` | bubbles | puzzle, pet, creative |
|
||||
| Waterfall | `nature_waterfall` | A cascading waterfall in a lush green cliff | refreshing | `#1B4332` `#2D6A4F` `#52B788` `#B7E4C7` | fog | puzzle, creative, story |
|
||||
| Meadow | `nature_meadow` | A sunny meadow with wildflowers and gentle breeze | joyful | `#4CAF50` `#81C784` `#A5D6A7` `#FFF59D` | leaves | pet, creative, story, puzzle |
|
||||
|
||||
---
|
||||
|
||||
### Urban Theme
|
||||
|
||||
City environments from skylines to subways.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| City Skyline | `urban_citySkyline` | City skyline silhouetted against a glowing sunset | inspiring | `#1A1A2E` `#2D2D44` `#4A4A66` `#FF8844` | dust | story, rpg, creative |
|
||||
| Street | `urban_street` | Busy city street with lamp posts and passing lights | busy | `#2C3E50` `#566573` `#808B96` `#FFD700` | dust | racing, action, sports |
|
||||
| Rooftop | `urban_rooftop` | Rooftop view over a twinkling nighttime city | reflective | `#1B2631` `#2C3E50` `#5D6D7E` `#F4D03F` | fireflies | story, puzzle, creative |
|
||||
| Neon District | `urban_neonDistrict` | Rain-slicked streets glowing with neon signs | electric | `#0D0D1A` `#1A0A2E` `#FF00FF` `#00FFCC` | rain | action, racing, music |
|
||||
| Subway | `urban_subway` | Underground subway platform with arriving train lights | gritty | `#1C1C1C` `#333333` `#555555` `#FFCC00` | dust | action, puzzle, rpg |
|
||||
| Park | `urban_park` | A peaceful city park with trees and walking paths | relaxed | `#2E7D32` `#4CAF50` `#81C784` `#795548` | leaves | pet, puzzle, story, creative |
|
||||
| Mall | `urban_mall` | Bright mall interior with shop windows and escalators | lively | `#E8E8E8` `#CCCCCC` `#888888` `#FF6B6B` | sparkles | trivia, creative, pet |
|
||||
| Alley | `urban_alley` | A narrow alley with a single warm light overhead | mysterious | `#111118` `#1E1E28` `#333340` `#BB8844` | fog | rpg, action, story |
|
||||
|
||||
---
|
||||
|
||||
### Fantasy Theme
|
||||
|
||||
Magical and fantastical environments.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Castle | `fantasy_castle` | A grand castle silhouette against a twilight sky with golden banners | heroic | `#1a1040` `#2d1b69` `#8b7355` `#ffd700` | fireflies | rpg, action, story |
|
||||
| Dungeon | `fantasy_dungeon` | Dark stone corridors lit by flickering torches and glowing embers | dark | `#0a0a0f` `#1a1520` `#4a3728` `#ff6a00` | particles | rpg, action, puzzle |
|
||||
| Enchanted Forest | `fantasy_enchantedForest` | A mystical forest with glowing mushrooms and magical light | mystical | `#0a1a0f` `#0d2818` `#1a4a2a` `#44ff88` | fireflies | rpg, story, puzzle |
|
||||
| Floating Islands | `fantasy_floatingIslands` | Majestic islands suspended in a violet sky with waterfalls | wonder | `#1a0a3a` `#2d1b69` `#5a8a5a` `#88ccff` | sparkles | rpg, puzzle, creative |
|
||||
| Magical Library | `fantasy_magicalLibrary` | Towering bookshelves with floating magical tomes and arcane symbols | scholarly | `#1a0f0a` `#2d1a10` `#6b4423` `#cc88ff` | sparkles | puzzle, trivia, story |
|
||||
| Crystal Cave | `fantasy_crystalCave` | An underground cavern filled with luminous crystal formations | ethereal | `#0a0a1a` `#0f1a2d` `#225588` `#44ddff` | sparkles | puzzle, rpg, creative |
|
||||
| Temple | `fantasy_temple` | An ancient temple with towering marble pillars and sacred golden light | sacred | `#1a1025` `#2d1a3a` `#c9b77a` `#ffd700` | dust | rpg, story, trivia |
|
||||
| Tower | `fantasy_tower` | A tall wizard tower under a starlit sky with arcane energy | arcane | `#0a0a20` `#151540` `#554488` `#aa66ff` | sparkles | rpg, puzzle, trivia |
|
||||
|
||||
---
|
||||
|
||||
### Abstract Theme
|
||||
|
||||
Non-representational visual patterns and effects.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Geometric Patterns | `abstract_geometricPatterns` | Interlocking geometric shapes in warm contrasting tones | focused | `#0d1b2a` `#1b2838` `#e07a5f` `#f2cc8f` | dust | puzzle, trivia, creative |
|
||||
| Gradients | `abstract_gradients` | Smoothly blending gradient orbs with ethereal color transitions | dreamy | `#0f0c29` `#302b63` `#24243e` `#ff6b6b` | sparkles | creative, story, music |
|
||||
| Particle Fields | `abstract_particleFields` | A dynamic network of connected particle nodes with pulsing energy | energetic | `#000000` `#111122` `#00ff88` `#0088ff` | particles | action, puzzle, physics |
|
||||
| Minimalist | `abstract_minimalist` | Clean open space with a single bold accent line | calm | `#f5f5f0` `#e8e4dd` `#2d2d2d` `#c44536` | none | puzzle, trivia, creative |
|
||||
| Waves | `abstract_waves` | Layered sinusoidal waves undulating in cool blue tones | flowing | `#0a192f` `#172a45` `#00b4d8` `#90e0ef` | bubbles | creative, music, physics |
|
||||
| Fractals | `abstract_fractals` | Recursive branching patterns that grow and pulse | complex | `#0a0a0a` `#1a1a2e` `#e94560` `#0f3460` | sparkles | puzzle, physics, creative |
|
||||
| Kaleidoscope | `abstract_kaleidoscope` | Symmetric radial patterns rotating like a living kaleidoscope | playful | `#1a1a2e` `#16213e` `#e94560` `#ffd166` | sparkles | music, creative, puzzle |
|
||||
| Matrix | `abstract_matrix` | Cascading columns of digital characters streaming down | digital | `#000000` `#001100` `#00ff41` `#008f11` | none | action, puzzle, trivia |
|
||||
|
||||
---
|
||||
|
||||
### Retro Theme
|
||||
|
||||
Nostalgic gaming aesthetics from past eras.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Pixel Grid | `retro_pixelGrid` | A chunky pixel grid with classic 8-bit color blocks | nostalgic | `#1a1a2e` `#16213e` `#00ff88` `#ff6600` | sparkles | action, puzzle, creative |
|
||||
| Arcade Cabinet | `retro_arcadeCabinet` | Inside view of a dark arcade with neon-lit cabinet edges | fun | `#0a0a15` `#1a0a2e` `#ff2277` `#ffcc00` | dust | action, racing, sports |
|
||||
| CRT Effects | `retro_crtEffects` | Authentic CRT monitor with phosphor glow and scan line interference | vintage | `#0a0a0a` `#0f1a0f` `#33ff33` `#88ff88` | none | action, puzzle, trivia |
|
||||
| Vaporwave | `retro_vaporwave` | Aesthetic sunset horizon with perspective grid and palm silhouettes | aesthetic | `#0d0221` `#261447` `#ff71ce` `#01cdfe` | stars | creative, music, racing |
|
||||
| Scanlines | `retro_scanlines` | Dense horizontal scan lines over a shifting color field | technical | `#0a0a1a` `#141428` `#00aaff` `#ff4444` | none | action, puzzle, physics |
|
||||
| Glitch | `retro_glitch` | Fragmented display with chromatic aberration and corrupted data | chaotic | `#0a0a0a` `#1a0a1a` `#ff0055` `#00ffcc` | particles | action, racing, physics |
|
||||
| 8-Bit | `retro_eightBit` | Charming 8-bit scene with blocky pixel clouds and hills | cheerful | `#2b2b6e` `#3d3d94` `#5bc0eb` `#fde74c` | none | action, puzzle, creative |
|
||||
| Synthwave | `retro_synthwave` | Outrun-style neon horizon with receding road and mountain silhouettes | epic | `#0d0015` `#1a0033` `#ff2975` `#f222ff` | stars | racing, action, sports |
|
||||
|
||||
---
|
||||
|
||||
### Indoor Theme
|
||||
|
||||
Interior environments and spaces.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Laboratory | `indoor_laboratory` | Dark science lab with glowing equipment and bubbling vials | mysterious | `#1a1e2e` `#2a3040` `#0f6b5e` `#44eebb` | bubbles | puzzle, trivia, rpg |
|
||||
| Classroom | `indoor_classroom` | Bright classroom with chalkboard, desks, and warm sunlight | cheerful | `#f5f0e0` `#d4c9a8` `#5b8c5a` `#8b5e3c` | dust | trivia, puzzle, story |
|
||||
| Bedroom | `indoor_bedroom` | Cozy bedroom at night with warm lamp glow and string lights | cozy | `#2a1f3d` `#3d2b5a` `#e8a87c` `#ffd166` | fireflies | story, creative, pet |
|
||||
| Kitchen | `indoor_kitchen` | Bright kitchen with checkered floor and warm appliance glow | warm | `#fff8e7` `#f0d9a0` `#c97b3a` `#8b4513` | dust | creative, pet, puzzle |
|
||||
| Arcade | `indoor_arcade` | Dark arcade with neon-lit cabinets and glowing screens | energetic | `#0d0d1a` `#1a0a2e` `#ff00ff` `#00ffff` | sparkles | action, racing, sports |
|
||||
| Office | `indoor_office` | Modern office with monitor glow and large windows | focused | `#e8e4dc` `#c5bfb2` `#4a6fa5` `#2c3e50` | dust | puzzle, trivia, creative |
|
||||
| Library | `indoor_library` | Grand library with towering bookshelves and reading lamps | serene | `#2c1810` `#4a3020` `#c4956a` `#f4e4c1` | dust | story, trivia, puzzle, rpg |
|
||||
| Gym | `indoor_gym` | Modern gym with equipment silhouettes and bright overhead lights | energetic | `#1a1a24` `#2a2a3a` `#ff4444` `#ff8800` | dust | action, sports, racing |
|
||||
|
||||
---
|
||||
|
||||
### Weather Theme
|
||||
|
||||
Atmospheric and meteorological conditions.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Rain | `weather_rain` | Overcast sky with steady rainfall, puddles, and muted city outlines | melancholy | `#3a4a5a` `#2a3a4a` `#5a6a7a` `#8899aa` | rain | story, puzzle, rpg |
|
||||
| Snow | `weather_snow` | Gentle snowfall over a soft winter landscape with frosted trees | peaceful | `#c8d8e8` `#a0b8cc` `#e8eef4` `#FFFFFF` | snow | puzzle, pet, creative, story |
|
||||
| Storm | `weather_storm` | Dark storm clouds with heavy rain, wind-bent trees, and thunder | intense | `#1a1a25` `#252535` `#3a3a55` `#667788` | rain | action, rpg, racing |
|
||||
| Sunset | `weather_sunset` | Vibrant sunset with orange-purple gradient and silhouette landscape | warm | `#1a0a2e` `#cc4400` `#ff8833` `#ffd166` | sparkles | story, creative, pet, music |
|
||||
| Sunrise | `weather_sunrise` | Early morning sunrise with pink-orange sky, mist, and dewy fields | hopeful | `#2a1a40` `#884466` `#ee8855` `#ffe4a0` | dust | puzzle, pet, creative, sports |
|
||||
| Fog | `weather_fog` | Thick fog with barely visible shapes and dim lights | eerie | `#8a8a8a` `#666666` `#aaaaaa` `#cccccc` | fog | story, rpg, puzzle |
|
||||
| Lightning | `weather_lightning` | Pitch-dark sky lit by dramatic lightning bolts | dramatic | `#0a0a18` `#1a1a30` `#3344aa` `#eeeeff` | rain | action, rpg, racing |
|
||||
| Aurora | `weather_aurora` | Dark arctic sky with shimmering aurora borealis | magical | `#0a0a1a` `#0d1b2a` `#22cc88` `#8844dd` | stars | creative, rpg, story, music |
|
||||
|
||||
---
|
||||
|
||||
### Sports Theme
|
||||
|
||||
Athletic venues and environments.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Stadium | `sports_stadium` | Large outdoor stadium with green pitch and bright floodlights | exciting | `#1a3a1a` `#2a5a2a` `#88cc44` `#ffffff` | sparkles | sports, action, trivia |
|
||||
| Race Track | `sports_raceTrack` | Asphalt race track with lane markings and grandstand | fast | `#333333` `#555555` `#cc0000` `#ffffff` | dust | racing, action, sports |
|
||||
| Court | `sports_court` | Indoor basketball court with polished hardwood and arena lighting | competitive | `#c47a34` `#e8944a` `#ffffff` `#2255aa` | sparkles | sports, action, trivia |
|
||||
| Field | `sports_field` | Open soccer field under blue sky with goals and corner flags | fresh | `#2a7a2a` `#3a9a3a` `#ffffff` `#88cc44` | leaves | sports, action, pet |
|
||||
| Arena | `sports_arena` | Dark fighting arena with dramatic spotlights and ropes | intense | `#1a1a2e` `#2a2a44` `#ff4444` `#ffaa00` | dust | action, sports, rpg |
|
||||
| Pool | `sports_pool` | Olympic swimming pool with lane dividers and rippling water | calm | `#1a6088` `#2288aa` `#44bbdd` `#88eeff` | sparkles | sports, racing, puzzle |
|
||||
| Gym | `sports_gym` | Gymnastics arena with spring floor and apparatus silhouettes | competitive | `#ddc8a0` `#c4a870` `#ffffff` `#cc3333` | dust | sports, action, creative |
|
||||
| Skatepark | `sports_skatepark` | Urban skatepark with concrete ramps and graffiti | cool | `#555566` `#777788` `#ff6600` `#ffcc00` | leaves | action, sports, racing, creative |
|
||||
|
||||
---
|
||||
|
||||
### Seasonal Theme
|
||||
|
||||
Time-of-year and holiday-themed environments.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Spring Meadow | `seasonal_springMeadow` | A sunny spring meadow with wildflowers and gentle hills | cheerful | `#87CEEB` `#98FB98` `#FFD700` `#FF69B4` | leaves | puzzle, creative, pet |
|
||||
| Autumn Leaves | `seasonal_autumnLeaves` | A warm autumn landscape with falling leaves and golden light | warm | `#2C1810` `#CC5500` `#DD8833` `#FFD700` | leaves | puzzle, story, rpg |
|
||||
| Winter Wonderland | `seasonal_winterWonderland` | A serene winter landscape with snow-covered hills | calm | `#1A2744` `#4A7CB5` `#C8E0F4` `#FFFFFF` | snow | puzzle, story, creative |
|
||||
| Summer Beach | `seasonal_summerBeach` | A vibrant summer beach scene with rolling waves and warm sand | energetic | `#0077BE` `#00BFFF` `#F4D03F` `#FF6347` | sparkles | action, sports, racing |
|
||||
| Harvest | `seasonal_harvest` | Golden harvest fields with wheat rows and warm sunset | warm | `#5C3317` `#D4A017` `#8B4513` `#FFD700` | dust | puzzle, story, creative |
|
||||
| Blossom | `seasonal_blossom` | Gentle cherry blossom petals drifting under pink-tinted skies | peaceful | `#FFB7C5` `#FF69B4` `#FFF0F5` `#98FB98` | leaves | puzzle, story, creative, music |
|
||||
| Snowfall | `seasonal_snowfall` | Heavy snowfall over a dark winter night with distant lights | calm | `#0D1B2A` `#1B2838` `#415A77` `#E0E1DD` | snow | puzzle, story, rpg |
|
||||
| Tropical | `seasonal_tropical` | A lush tropical scene with dense foliage and vivid colors | energetic | `#00CED1` `#FF6347` `#228B22` `#FFD700` | fireflies | action, racing, sports, pet |
|
||||
|
||||
---
|
||||
|
||||
### Underwater Theme
|
||||
|
||||
Aquatic and marine environments.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Coral Reef | `underwater_coralReef` | A vibrant coral reef teeming with colorful marine life | cheerful | `#006994` `#00A8CC` `#FF6F61` `#FFB347` | bubbles | puzzle, pet, creative |
|
||||
| Deep Sea | `underwater_deepSea` | The mysterious deep sea with bioluminescent creatures | mysterious | `#0A0A2E` `#0D1B3E` `#1A3A5C` `#00FFCC` | fireflies | rpg, story, puzzle |
|
||||
| Kelp Forest | `underwater_kelpForest` | A towering kelp forest with dappled light filtering through | peaceful | `#0B4F3A` `#1A7A5C` `#2FA87A` `#7EC8A0` | bubbles | puzzle, creative, story |
|
||||
| Submarine | `underwater_submarine` | A submarine expedition through dark ocean depths | adventurous | `#0A1628` `#1A3050` `#D4AA44` `#CC4444` | bubbles | action, rpg, story |
|
||||
| Shipwreck | `underwater_shipwreck` | A sunken ship resting on the ocean floor, overgrown with sea life | mysterious | `#0A1A2A` `#1A3344` `#8B7355` `#44AA88` | bubbles | rpg, story, puzzle |
|
||||
| Abyss | `underwater_abyss` | The abyssal zone with volcanic vents and glowing magma cracks | dark | `#020208` `#050515` `#0A0A30` `#FF2200` | particles | action, rpg, story |
|
||||
| Ice Cave | `underwater_iceCave` | An underwater ice cavern with crystalline formations | calm | `#0A2A4A` `#1A5A8A` `#88CCEE` `#E0F0FF` | sparkles | puzzle, story, creative |
|
||||
| Treasure | `underwater_treasure` | A hidden underwater treasure trove glowing with golden riches | adventurous | `#0D2B45` `#1A4A6A` `#FFD700` `#FF8C00` | sparkles | rpg, action, puzzle |
|
||||
|
||||
---
|
||||
|
||||
### Sky Theme
|
||||
|
||||
Aerial and atmospheric perspectives.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Clouds | `sky_clouds` | A bright blue sky filled with drifting puffy white clouds | peaceful | `#4A90D9` `#87CEEB` `#FFFFFF` `#F0F8FF` | dust | puzzle, creative, pet |
|
||||
| Flying High | `sky_flyingHigh` | High altitude perspective with streaking wind lines and brilliant sun | energetic | `#1E3A6E` `#4682B4` `#FFD700` `#FFFFFF` | sparkles | action, racing, sports |
|
||||
| Hot Air Balloon | `sky_hotAirBalloon` | Colorful hot air balloons floating gently across a warm sky | cheerful | `#87CEEB` `#FF6347` `#FFD700` `#FFFFFF` | dust | puzzle, creative, pet, music |
|
||||
| Airplane View | `sky_airplaneView` | View from an airplane window looking down at a sea of clouds | calm | `#1A3366` `#336699` `#FFFFFF` `#AACCEE` | dust | puzzle, story, creative |
|
||||
| Stratosphere | `sky_stratosphere` | The edge of space where the atmosphere fades and Earth curves below | mysterious | `#0A0A2E` `#1A1A5E` `#4444AA` `#8888DD` | stars | story, rpg, puzzle |
|
||||
| Bird's Eye | `sky_birdsEye` | A bird's eye view looking down at patchwork fields and winding rivers | peaceful | `#5588BB` `#88BBDD` `#228B22` `#DEB887` | dust | puzzle, creative, story |
|
||||
| Horizon | `sky_horizon` | A dramatic sunset horizon with layers of warm orange, pink, and gold | warm | `#1A0533` `#CC4488` `#FF8844` `#FFD700` | dust | story, music, creative, rpg |
|
||||
| Starfield | `sky_starfield` | A clear night sky bursting with stars and a faint galactic band | calm | `#000011` `#0A0A2A` `#1A1A44` `#FFFFFF` | stars | puzzle, story, rpg, music |
|
||||
|
||||
---
|
||||
|
||||
### Mechanical Theme
|
||||
|
||||
Industrial and technological environments.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Gears | `mechanical_gears` | Interlocking gears turning against a dark steel backdrop | focused | `#1A1A24` `#2A2A38` `#7A7A88` `#C08040` | dust | puzzle, physics, rpg |
|
||||
| Circuits | `mechanical_circuits` | Circuit board traces with glowing data paths | focused | `#0A0F14` `#112222` `#00CC88` `#33FFAA` | sparkles | puzzle, trivia, physics |
|
||||
| Factory | `mechanical_factory` | Factory floor with smokestacks and warning stripes | busy | `#1A1510` `#2A2520` `#8B7355` `#DDAA44` | dust | action, physics, puzzle |
|
||||
| Robot Factory | `mechanical_robotFactory` | Robot assembly line with mechanical arms and sparks | playful | `#151520` `#222238` `#5588CC` `#FFAA33` | sparkles | action, puzzle, creative |
|
||||
| Conveyor Belts | `mechanical_conveyorBelts` | Layered conveyor belts moving crates and parts | busy | `#18181F` `#28283A` `#666680` `#DD8822` | dust | puzzle, action, physics |
|
||||
| Pipes | `mechanical_pipes` | Interconnected pipe network with valves and joints | complex | `#121218` `#1E2A1E` `#558855` `#88BB44` | dust | puzzle, physics, rpg |
|
||||
| Steampunk | `mechanical_steamPunk` | Brass gears, gauges, and steam vents in warm tones | adventurous | `#1A1008` `#2A1A0A` `#C08040` `#E8C060` | dust | rpg, story, action |
|
||||
| Tech Lab | `mechanical_techLab` | Clean tech laboratory with holographic displays | curious | `#0E0E1A` `#1A1A30` `#4455AA` `#66DDFF` | sparkles | puzzle, trivia, creative |
|
||||
|
||||
---
|
||||
|
||||
### Spooky Theme
|
||||
|
||||
Eerie and horror-themed environments.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Haunted House | `spooky_hauntedHouse` | A looming haunted house silhouette against a moonlit sky | eerie | `#0A0812` `#1A1025` `#3A2255` `#CCCC88` | fog | story, puzzle, rpg |
|
||||
| Graveyard | `spooky_graveyard` | Moonlit graveyard with crooked tombstones and mist | somber | `#080810` `#121828` `#2A3348` `#99AA77` | fog | rpg, story, puzzle |
|
||||
| Dark Forest | `spooky_darkForest` | Dense dark forest with twisted trees and glowing eyes | foreboding | `#050808` `#0A1510` `#1A2A18` `#3A5530` | fireflies | rpg, action, story |
|
||||
| Abandoned Building | `spooky_abandonedBuilding` | Crumbling abandoned building with broken windows and debris | desolate | `#0A0A0E` `#181820` `#3A3A48` `#667766` | dust | action, puzzle, rpg |
|
||||
| Crypt | `spooky_crypt` | Underground stone crypt with torchlight and coffins | dread | `#060608` `#101015` `#2A2A35` `#558855` | dust | rpg, action, story |
|
||||
| Manor | `spooky_manor` | Gothic manor interior with candlelight and portraits | gothic | `#0A080C` `#1A1020` `#3A2040` `#886644` | dust | story, rpg, puzzle |
|
||||
| Fog | `spooky_fog` | Thick rolling fog obscuring dark shapes and dim lights | mysterious | `#0A0E12` `#151E28` `#2A3844` `#556688` | fog | story, puzzle, rpg |
|
||||
| Shadows | `spooky_shadows` | Near-total darkness with shifting shadows and faint light | paranoid | `#050506` `#0E0E14` `#1C1C2A` `#332244` | dust | action, puzzle, rpg |
|
||||
|
||||
---
|
||||
|
||||
### Colorful Theme
|
||||
|
||||
Vibrant and visually striking environments.
|
||||
|
||||
| Variant | ID | Description | Mood | Colors | Particles | Recommended For |
|
||||
|---------|-----|-------------|------|--------|-----------|-----------------|
|
||||
| Rainbow | `colorful_rainbow` | Bold rainbow arcs and bands across a bright sky | joyful | `#FF4444` `#FFAA33` `#FFEE33` `#44DD55` | sparkles | creative, puzzle, music |
|
||||
| Paint Splatter | `colorful_paintSplatter` | Colorful paint splatters on a light canvas | creative | `#FAFAE8` `#FF3366` `#3388FF` `#FFCC00` | particles | creative, music, action |
|
||||
| Candy World | `colorful_candyWorld` | Candy landscape with lollipops, gumdrops, and sugar hills | sweet | `#FFE0F0` `#FF88BB` `#88DDFF` `#AAFFAA` | sparkles | pet, creative, puzzle |
|
||||
| Disco | `colorful_disco` | Disco floor with mirror ball and sweeping colored lights | energetic | `#1A0A2E` `#2A1A44` `#FF33AA` `#33FFCC` | sparkles | music, action, sports |
|
||||
| Neon | `colorful_neon` | Neon signs and glowing outlines against dark city night | electric | `#0A0A18` `#151530` `#FF0088` `#00FFCC` | particles | action, racing, music |
|
||||
| Tie-Dye | `colorful_tieDye` | Swirling tie-dye patterns with organic color blending | psychedelic | `#FF4488` `#FFAA22` `#44DDBB` `#8844FF` | particles | creative, music, story |
|
||||
| Gradient | `colorful_gradient` | Smooth shifting color gradients with geometric accents | peaceful | `#FF6B6B` `#FECA57` `#48DBFB` `#FF9FF3` | sparkles | puzzle, creative, trivia |
|
||||
| Prism | `colorful_prism` | Light splitting through prisms into rainbow beams | mystical | `#1A1A2E` `#0F3460` `#E94560` `#FFFFFF` | sparkles | puzzle, physics, creative |
|
||||
|
||||
---
|
||||
|
||||
## Compatible Music Moods
|
||||
|
||||
Backgrounds are tagged with compatible music moods for cohesive game experiences:
|
||||
|
||||
| Music Mood | Example Backgrounds |
|
||||
|------------|---------------------|
|
||||
| ambient | space_nebula, nature_ocean, weather_fog |
|
||||
| calm | nature_meadow, weather_snow, sky_clouds |
|
||||
| dreamy | abstract_gradients, seasonal_blossom, weather_aurora |
|
||||
| energetic | space_warpSpeed, sports_stadium, colorful_disco |
|
||||
| epic | fantasy_castle, weather_sunset, mechanical_steamPunk |
|
||||
| electronic | abstract_particleFields, retro_synthwave, colorful_neon |
|
||||
| playful | colorful_candyWorld, retro_pixelGrid, mechanical_robotFactory |
|
||||
| tense | space_asteroidField, spooky_crypt, weather_storm |
|
||||
| mysterious | underwater_deepSea, spooky_fog, sky_stratosphere |
|
||||
| happy | seasonal_springMeadow, sky_hotAirBalloon, underwater_treasure |
|
||||
|
||||
---
|
||||
|
||||
## Compatible Avatar Contexts
|
||||
|
||||
Recommended avatar context pairings for backgrounds:
|
||||
|
||||
| Background Theme | Recommended Avatar Contexts |
|
||||
|------------------|----------------------------|
|
||||
| space | astronaut, robot, alien, superhero |
|
||||
| nature | explorer, hiker, animal, fairy |
|
||||
| urban | cyclist, skater, runner, casual |
|
||||
| fantasy | knight, wizard, fairy, dragon |
|
||||
| abstract | geometric, minimal, gamer |
|
||||
| retro | pixel, arcade, classic |
|
||||
| indoor | student, worker, gamer, casual |
|
||||
| weather | raincoat, winter, summer |
|
||||
| sports | athlete, racer, swimmer |
|
||||
| seasonal | holiday, themed |
|
||||
| underwater | diver, mermaid, fish |
|
||||
| sky | pilot, bird, balloon |
|
||||
| mechanical | robot, engineer, steampunk |
|
||||
| spooky | ghost, monster, vampire |
|
||||
| colorful | party, disco, rainbow |
|
||||
|
||||
---
|
||||
|
||||
## API Usage
|
||||
|
||||
### Get All Themes
|
||||
|
||||
```javascript
|
||||
const summary = BackgroundEngine.getThemeSummary();
|
||||
// Returns: [{ theme: 'space', variants: [...], count: 8 }, ...]
|
||||
```
|
||||
|
||||
### Get Total Background Count
|
||||
|
||||
```javascript
|
||||
const total = BackgroundEngine.getTotalCount();
|
||||
// Returns: 120
|
||||
```
|
||||
|
||||
### Find Backgrounds by Game Genre
|
||||
|
||||
```javascript
|
||||
const puzzleBackgrounds = BackgroundEngine.findByGenre('puzzle');
|
||||
// Returns array of backgrounds recommended for puzzle games
|
||||
```
|
||||
|
||||
### Find by Music Mood
|
||||
|
||||
```javascript
|
||||
const calmBackgrounds = BackgroundEngine.findByMusicMood('calm');
|
||||
// Returns backgrounds compatible with calm music
|
||||
```
|
||||
|
||||
### Get Random Background from Theme
|
||||
|
||||
```javascript
|
||||
const bg = BackgroundEngine.getRandomFromTheme('space');
|
||||
// Returns: { theme: 'space', variant: 'nebula' } (random)
|
||||
```
|
||||
|
||||
### Get Random Matching Filters
|
||||
|
||||
```javascript
|
||||
const bg = BackgroundEngine.getRandomMatching({
|
||||
mood: 'energetic',
|
||||
tags: ['neon']
|
||||
});
|
||||
```
|
||||
|
||||
### Validate Catalog Integrity
|
||||
|
||||
```javascript
|
||||
const issues = BackgroundEngine.validateCatalog();
|
||||
// Returns array of any missing/invalid registrations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Visual Preview Guide
|
||||
|
||||
Each background can be previewed using the BackgroundEngine:
|
||||
|
||||
```javascript
|
||||
// Initialize canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 800;
|
||||
canvas.height = 600;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Render a specific background
|
||||
BackgroundEngine.set('space', 'nebula');
|
||||
|
||||
// In animation loop:
|
||||
function render(time) {
|
||||
BackgroundEngine.render(ctx, canvas.width, canvas.height, time / 1000);
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
render(0);
|
||||
```
|
||||
|
||||
### Color Palette Structure
|
||||
|
||||
Each background defines 4 colors:
|
||||
- **Color 1**: Primary/dominant background color
|
||||
- **Color 2**: Secondary background color
|
||||
- **Color 3**: Accent/highlight color
|
||||
- **Color 4**: Detail/contrast color
|
||||
|
||||
### Render Layers
|
||||
|
||||
1. **Base Layer** (`renderBase`) - Sky, ground, gradients
|
||||
2. **Mid Layer** (`renderMid`) - Objects, details, animations
|
||||
3. **Particle Layer** - Ambient particle effects (optional)
|
||||
|
||||
---
|
||||
|
||||
## Catalog Statistics
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total Themes | 15 |
|
||||
| Variants Per Theme | 8 |
|
||||
| Total Backgrounds | 120 |
|
||||
| Particle Types | 10 |
|
||||
| Moods Available | 16 |
|
||||
| Game Genres Mapped | 10 |
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2026-02-05*
|
||||
*Version: 1.0.0*
|
||||
@@ -0,0 +1,721 @@
|
||||
# Context Catalog
|
||||
|
||||
## Overview
|
||||
|
||||
26 game contexts that place avatars in themed scenarios.
|
||||
3 categories: Vehicles (12), Costumes (10), Pure (4).
|
||||
|
||||
Each context defines:
|
||||
- **Avatar Mode**: How much of the avatar is visible (HEAD_ONLY, HEAD_AND_SHOULDERS, UPPER_BODY, FULL_BODY)
|
||||
- **Animations**: Available animation states for the avatar
|
||||
- **Accessory Visibility**: Which avatar accessories (eyewear, headwear, effects) are visible
|
||||
- **Effects**: Visual effects triggered by game states
|
||||
- **Recommended For**: Game types that work well with this context
|
||||
|
||||
---
|
||||
|
||||
## Vehicle Contexts (12)
|
||||
|
||||
Vehicles place the avatar inside cockpits, vehicles, or as part of a flying/riding scenario.
|
||||
|
||||
---
|
||||
|
||||
### spaceship-cockpit
|
||||
|
||||
- **ID**: `spaceship-cockpit`
|
||||
- **Mode**: HEAD_ONLY
|
||||
- **Description**: Pilot a spacecraft through space
|
||||
- **Background**: Dark space with animated starfield, cockpit interior frame with side panels
|
||||
- **Foreground**: Cockpit glass with blue tint, HUD targeting reticle, control panel with blinking buttons, speed indicator
|
||||
- **Animations**: idle, bob, celebrate, hurt
|
||||
- **Effects**:
|
||||
- `boost`: Particle flames from rear
|
||||
- `damage`: Screen shake (intensity 5)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Space shooters, sci-fi games, arcade
|
||||
- **Recommended Music**: Epic/Intense
|
||||
- **Recommended Backgrounds**: Space, nebula, asteroid fields
|
||||
|
||||
---
|
||||
|
||||
### race-car
|
||||
|
||||
- **ID**: `race-car`
|
||||
- **Mode**: HEAD_AND_SHOULDERS
|
||||
- **Description**: Race at high speed on the track
|
||||
- **Background**: Sky gradient, road rushing past with animated lane markers, speed lines on sides
|
||||
- **Foreground**: Dashboard with curve, steering wheel, side mirrors with road reflections, speedometer and RPM gauges with animated needles
|
||||
- **Animations**: idle, bob, celebrate, hurt, turn-left, turn-right
|
||||
- **Effects**:
|
||||
- `accelerate`: Vertical blur
|
||||
- `drift`: Side particle effects
|
||||
- `damage`: Screen shake (intensity 8)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Racing games, driving simulations, arcade racers
|
||||
- **Recommended Music**: Intense, electronic
|
||||
- **Recommended Backgrounds**: Urban, track, highway
|
||||
|
||||
---
|
||||
|
||||
### airplane
|
||||
|
||||
- **ID**: `airplane`
|
||||
- **Mode**: HEAD_AND_SHOULDERS
|
||||
- **Description**: Fly through the clouds as a pilot
|
||||
- **Background**: Sky gradient (blue), animated clouds streaming past, cockpit interior sides
|
||||
- **Foreground**: Windshield frame with center divider, instrument panel with 5 gauges (animated needles), yoke/control column, altitude and heading displays
|
||||
- **Animations**: idle, bob, celebrate, hurt
|
||||
- **Effects**:
|
||||
- `turbulence`: Screen shake (intensity 3)
|
||||
- `dive`: Vertical blur
|
||||
- `damage`: Screen shake (intensity 10)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Flight simulators, arcade flyers
|
||||
- **Recommended Music**: Adventurous, orchestral
|
||||
- **Recommended Backgrounds**: Clouds, sky, mountains
|
||||
|
||||
---
|
||||
|
||||
### flappy-style
|
||||
|
||||
- **ID**: `flappy-style`
|
||||
- **Mode**: HEAD_ONLY
|
||||
- **Description**: Flap through obstacles as a flying character
|
||||
- **Background**: Transparent (game provides its own)
|
||||
- **Foreground**: Animated wing sprites on left and right sides with flapping motion, wing detail lines
|
||||
- **Animations**: idle, flap, fall, hurt
|
||||
- **Effects**:
|
||||
- `flap`: Vertical blur (intensity 2)
|
||||
- `fall`: Downward stretch
|
||||
- `damage`: Red flash
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear hidden, Effects visible
|
||||
- **Best For**: Flappy-style games, casual mobile games, arcade
|
||||
- **Recommended Music**: Playful, casual
|
||||
- **Recommended Backgrounds**: Sky, pipes, obstacles
|
||||
|
||||
---
|
||||
|
||||
### tank
|
||||
|
||||
- **ID**: `tank`
|
||||
- **Mode**: HEAD_ONLY
|
||||
- **Description**: Command a battle tank
|
||||
- **Background**: Military green interior with radial gradient, riveted panel details, side screens with static effect
|
||||
- **Foreground**: Circular tank hatch frame with bolts, periscope view with crosshair at top, control panel with warning lights, ammo counter display
|
||||
- **Animations**: idle, bob, celebrate, hurt, aim
|
||||
- **Effects**:
|
||||
- `fire`: Yellow flash (intensity 10)
|
||||
- `rumble`: Screen shake (intensity 4)
|
||||
- `damage`: Screen shake (intensity 12)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Military shooters, strategy games, arcade combat
|
||||
- **Recommended Music**: Intense, military
|
||||
- **Recommended Backgrounds**: Battlefield, desert, urban warfare
|
||||
|
||||
---
|
||||
|
||||
### pacman-style
|
||||
|
||||
- **ID**: `pacman-style`
|
||||
- **Mode**: HEAD_ONLY
|
||||
- **Description**: Navigate mazes and eat pellets
|
||||
- **Background**: Transparent (game provides maze)
|
||||
- **Foreground**: Subtle mouth/chomp overlay effect
|
||||
- **Animations**: idle, chomp, hurt, power
|
||||
- **Effects**:
|
||||
- `chomp`: Scale pulse (rate 12)
|
||||
- `power`: Cyan glow
|
||||
- `damage`: Red flash
|
||||
- **Accessory Visibility**: Eyewear hidden, Headwear hidden, Effects visible
|
||||
- **Best For**: Maze games, arcade classics, retro games
|
||||
- **Recommended Music**: Retro, chiptune
|
||||
- **Recommended Backgrounds**: Maze walls, pellets
|
||||
|
||||
---
|
||||
|
||||
### hoverboard
|
||||
|
||||
- **ID**: `hoverboard`
|
||||
- **Mode**: UPPER_BODY
|
||||
- **Description**: Ride a futuristic hoverboard
|
||||
- **Background**: Ground rushing beneath with speed lines, shadow beneath board
|
||||
- **Foreground**: Animated hoverboard with hover glow effect, board body with gradient, cyan lights, hover jets with flicker
|
||||
- **Animations**: idle, bob, celebrate, hurt, lean-left, lean-right, jump
|
||||
- **Effects**:
|
||||
- `idle`: Bob motion (amplitude 3, rate 4)
|
||||
- `boost`: Cyan particle trail behind
|
||||
- `jump`: Upward stretch
|
||||
- `damage`: Screen shake (intensity 6)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Endless runners, racing games, arcade
|
||||
- **Recommended Music**: Electronic, futuristic
|
||||
- **Recommended Backgrounds**: City streets, futuristic
|
||||
|
||||
---
|
||||
|
||||
### jetpack
|
||||
|
||||
- **ID**: `jetpack`
|
||||
- **Mode**: UPPER_BODY
|
||||
- **Description**: Fly with a jetpack strapped to your back
|
||||
- **Background**: Sky gradient, clouds below, jetpack unit behind avatar (main body, fuel gauge, exhaust nozzles)
|
||||
- **Foreground**: Animated jet flames with flicker, smoke trail particles
|
||||
- **Animations**: idle, bob, celebrate, hurt, boost
|
||||
- **Effects**:
|
||||
- `idle`: Bob motion (amplitude 2, rate 2)
|
||||
- `boost`: Orange particle trail downward
|
||||
- `damage`: Screen shake (intensity 8)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Flying games, endless flyers, arcade shooters
|
||||
- **Recommended Music**: Action, adventure
|
||||
- **Recommended Backgrounds**: Sky, clouds, obstacles
|
||||
|
||||
---
|
||||
|
||||
### mech-suit
|
||||
|
||||
- **ID**: `mech-suit`
|
||||
- **Mode**: HEAD_ONLY
|
||||
- **Description**: Pilot a powerful mech suit
|
||||
- **Background**: Mech interior with radial gradient, side screens (radar with sweep, system status), interior frame elements
|
||||
- **Foreground**: Hexagonal chest window frame, HUD targeting brackets, control panel with joysticks, warning lights (blinking), status text
|
||||
- **Animations**: idle, bob, celebrate, hurt
|
||||
- **Effects**:
|
||||
- `walk`: Screen shake (intensity 2)
|
||||
- `fire`: Yellow flash
|
||||
- `damage`: Screen shake (intensity 10)
|
||||
- **Accessory Visibility**: Eyewear hidden, Headwear hidden, Effects visible
|
||||
- **Best For**: Mech combat, shooters, action games
|
||||
- **Recommended Music**: Epic, industrial
|
||||
- **Recommended Backgrounds**: City destruction, battlefield
|
||||
|
||||
---
|
||||
|
||||
### submarine
|
||||
|
||||
- **ID**: `submarine`
|
||||
- **Mode**: HEAD_AND_SHOULDERS
|
||||
- **Description**: Explore the depths in a submarine
|
||||
- **Background**: Underwater gradient (dark blue), rising bubbles with wobble animation, submarine interior with pipes and joints
|
||||
- **Foreground**: Circular porthole frame with rivets, glass reflection, water distortion overlay, control panel with depth gauge, pressure gauge, sonar ping effect, periscope controls
|
||||
- **Animations**: idle, bob, celebrate, hurt
|
||||
- **Effects**:
|
||||
- `idle`: Bob motion (amplitude 2, rate 1)
|
||||
- `pressure`: Distortion effect (intensity 3)
|
||||
- `damage`: Screen shake (intensity 6)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Underwater exploration, arcade games
|
||||
- **Recommended Music**: Ambient, mysterious
|
||||
- **Recommended Backgrounds**: Ocean depths, coral, sea creatures
|
||||
|
||||
---
|
||||
|
||||
### dragon-rider
|
||||
|
||||
- **ID**: `dragon-rider`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Soar through the skies on a dragon
|
||||
- **Background**: Sunset sky gradient, clouds, dragon neck with scales, animated wings (flapping), saddle beneath rider
|
||||
- **Foreground**: Dragon horns and ears framing view, dragon body/tail below, dragon legs with claws
|
||||
- **Animations**: idle, bob, celebrate, hurt, lean
|
||||
- **Effects**:
|
||||
- `idle`: Bob motion (amplitude 5, rate 2)
|
||||
- `fireBreath`: Orange glow
|
||||
- `dive`: Vertical blur
|
||||
- `damage`: Screen shake (intensity 8)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Fantasy adventures, flying games, RPGs
|
||||
- **Recommended Music**: Epic, fantasy orchestral
|
||||
- **Recommended Backgrounds**: Mountains, castles, sky
|
||||
|
||||
---
|
||||
|
||||
### motorcycle
|
||||
|
||||
- **ID**: `motorcycle`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Race on a high-speed motorcycle
|
||||
- **Background**: Sunset sky, road with animated lane markers, environment blur on sides, motion blur lines
|
||||
- **Foreground**: Full motorcycle with body, fuel tank with stripe, engine with details, exhaust pipes, front and rear wheels with spinning spokes, handlebars, mirrors, headlight, seat, wheel speed blur
|
||||
- **Animations**: idle, bob, celebrate, hurt, lean-left, lean-right
|
||||
- **Effects**:
|
||||
- `accelerate`: Vertical blur
|
||||
- `wheelie`: Rotation (-15 degrees)
|
||||
- `lean`: Rotation (20 degrees)
|
||||
- `damage`: Screen shake (intensity 8)
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Racing games, endless runners, arcade action
|
||||
- **Recommended Music**: Rock, intense
|
||||
- **Recommended Backgrounds**: Highway, city streets
|
||||
|
||||
---
|
||||
|
||||
## Costume Contexts (10)
|
||||
|
||||
Costumes dress the avatar in themed outfits with props and accessories.
|
||||
|
||||
---
|
||||
|
||||
### knight-armor
|
||||
|
||||
- **ID**: `knight-armor`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Medieval knight in shining armor
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Flowing red cape with gold trim, silver chestplate with ridge details, leg armor (greaves) with knee guards, armored boots
|
||||
- **Over Layer**: Shoulder pauldrons with spikes, helmet with visor (eye slits), red helmet crest
|
||||
- **Props**:
|
||||
- Sword (right hand): Silver blade, gold guard, leather handle
|
||||
- Shield (left arm): Silver with red emblem and gold cross
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `attack`: Silver slash
|
||||
- `hurt`: Orange sparks
|
||||
- **Accessory Visibility**: Eyewear hidden (behind visor), Headwear visible (above helmet), Effects visible
|
||||
- **Best For**: RPGs, medieval games, action adventures
|
||||
- **Recommended Music**: Epic, heroic
|
||||
|
||||
---
|
||||
|
||||
### space-suit
|
||||
|
||||
- **ID**: `space-suit`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Astronaut ready for space exploration
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: White suit body with blue accent stripes, oxygen pack on back with blinking lights, legs with stripes, magnetic boots
|
||||
- **Over Layer**: Transparent helmet bubble with reflection, helmet ring, blue accent dome, antenna with red tip, white gloves with blue centers
|
||||
- **Props**: None
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `hurt`: Cyan sparks
|
||||
- **Accessory Visibility**: Eyewear visible (through helmet), Headwear visible (above helmet), Effects visible
|
||||
- **Best For**: Action games, adventure, physics puzzles
|
||||
- **Recommended Music**: Ambient, electronic
|
||||
|
||||
---
|
||||
|
||||
### ninja-outfit
|
||||
|
||||
- **ID**: `ninja-outfit`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Stealthy ninja warrior
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Black body suit, katana on back (scabbard with red handle wrap, gold guard), gray belt with throwing stars, black leg wraps, tabi boots (split-toe)
|
||||
- **Over Layer**: Black arm wraps, face mask covering lower face, red headband with flowing tail (animated)
|
||||
- **Props**:
|
||||
- Katana (on back)
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `attack`: Dark slash
|
||||
- `hurt`: Gray smoke
|
||||
- **Accessory Visibility**: Eyewear visible (through eye slit), Headwear hidden (mask covers), Effects visible
|
||||
- **Best For**: Action games, stealth adventures, RPGs
|
||||
- **Recommended Music**: Tense, mysterious
|
||||
|
||||
---
|
||||
|
||||
### wizard-robes
|
||||
|
||||
- **ID**: `wizard-robes`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Mystical wizard with flowing robes
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Dark blue flowing robe (animated), inner fold, leather belt with gold buckle, side pouches, glowing purple runes on fabric (pulsing)
|
||||
- **Over Layer**: Wide sleeves, hood/collar, staff in right hand
|
||||
- **Props**:
|
||||
- Staff (right hand): Brown wooden shaft, purple orb with glow effect
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `attack`: Purple magic burst
|
||||
- `celebrate`: Gold sparkles
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible (optional wizard hat), Effects visible
|
||||
- **Best For**: RPGs, puzzle games, adventures
|
||||
- **Recommended Music**: Mystical, fantasy
|
||||
|
||||
---
|
||||
|
||||
### superhero-cape
|
||||
|
||||
- **ID**: `superhero-cape`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Classic superhero with billowing cape
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Billowing cape (animated with waves), bodysuit in avatar's clothing color, gold belt with white buckle, colored trunks, legs in suit color, colored boots with gold tops
|
||||
- **Over Layer**: Suit arms, colored gloves with gold cuffs, gold chest emblem (star shape), colored mask with white eye holes
|
||||
- **Props**: None
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `attack`: Yellow impact burst
|
||||
- `celebrate`: Gold sparkles
|
||||
- **Accessory Visibility**: Eyewear visible (as mask), Headwear visible, Effects visible
|
||||
- **Best For**: Action games, adventure
|
||||
- **Recommended Music**: Heroic, triumphant
|
||||
|
||||
---
|
||||
|
||||
### athlete-uniform
|
||||
|
||||
- **ID**: `athlete-uniform`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Athletic sports uniform with number 23
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Red jersey with white stripes, number 23 on back, dark shorts with red side stripes, exposed legs, white socks with red stripes, black athletic shoes with red accents
|
||||
- **Over Layer**: Red sleeves with white stripes, red headband, white wristbands, small number 23 on front
|
||||
- **Props**:
|
||||
- Ball (optional, right arm)
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `celebrate`: Gold confetti
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible, Effects visible
|
||||
- **Best For**: Sports games, action, racing
|
||||
- **Recommended Music**: Energetic, pump-up
|
||||
|
||||
---
|
||||
|
||||
### pirate-outfit
|
||||
|
||||
- **ID**: `pirate-outfit`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Swashbuckling pirate captain
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Dark red long coat (animated back), white ruffled shirt, leather belt with gold buckle, dark pants, brown boots with folded tops
|
||||
- **Over Layer**: Coat front panels with gold trim and buttons, cutlass in right hand (curved blade, gold guard, leather handle), black tricorn hat with gold trim and skull emblem
|
||||
- **Props**:
|
||||
- Cutlass (right hand)
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `attack`: Silver slash
|
||||
- **Accessory Visibility**: Eyewear visible (eyepatch optional), Headwear visible, Effects visible
|
||||
- **Best For**: Adventure games, action, RPGs
|
||||
- **Recommended Music**: Adventurous, sea shanty
|
||||
|
||||
---
|
||||
|
||||
### scientist-labcoat
|
||||
|
||||
- **ID**: `scientist-labcoat`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Mad scientist ready to experiment
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Blue shirt with dark tie, dark gray pants, brown sensible shoes
|
||||
- **Over Layer**: White lab coat (open front with collar), sleeves, pocket protector with colored pens (blue, red, black), ID badge with photo, safety goggles on forehead
|
||||
- **Props**:
|
||||
- Beaker (right hand): Glass container with bubbling green liquid and animated bubbles
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**:
|
||||
- `attack`: Green bubbles
|
||||
- **Accessory Visibility**: Eyewear visible (can wear both goggles and glasses), Headwear visible, Effects visible
|
||||
- **Best For**: Puzzle games, physics games, creative games
|
||||
- **Recommended Music**: Quirky, experimental
|
||||
|
||||
---
|
||||
|
||||
### chef-outfit
|
||||
|
||||
- **ID**: `chef-outfit`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Master chef ready to cook
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: White double-breasted chef jacket with buttons, white apron with strings and pocket, dark pants, black shoes
|
||||
- **Over Layer**: White sleeves, red kitchen towel over shoulder with white stripes, tall white chef hat (toque) with puffy top, utensils in apron pocket
|
||||
- **Props**:
|
||||
- Spatula (right hand): Brown handle, metal slotted head
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**: None
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible (under chef hat), Effects visible
|
||||
- **Best For**: Cooking games, creative games, puzzle
|
||||
- **Recommended Music**: Upbeat, cheerful
|
||||
|
||||
---
|
||||
|
||||
### cowboy-gear
|
||||
|
||||
- **ID**: `cowboy-gear`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Wild west cowboy
|
||||
- **Costume Details**:
|
||||
- **Under Layer**: Tan shirt with pointed collar, brown vest, leather belt with large gold oval buckle, blue jeans with seam details, brown cowboy boots with heels and silver spurs
|
||||
- **Over Layer**: Tan sleeves, red bandana around neck with knot and animated tails, brown cowboy hat with curved brim and gold buckle on band
|
||||
- **Props**:
|
||||
- Lasso (right hand, optional)
|
||||
- **Animations**: idle, walk, attack, hurt, celebrate
|
||||
- **Effects**: None
|
||||
- **Accessory Visibility**: Eyewear visible, Headwear visible (under hat), Effects visible
|
||||
- **Best For**: Western action, adventure games
|
||||
- **Recommended Music**: Western, country
|
||||
|
||||
---
|
||||
|
||||
## Pure Contexts (4)
|
||||
|
||||
Pure contexts apply minimal modifications to the avatar, using only effects and subtle accessories. Avatar is fully visible in FULL_BODY mode.
|
||||
|
||||
---
|
||||
|
||||
### platformer-standard
|
||||
|
||||
- **ID**: `platformer-standard`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Standard platforming character with no costume modifications
|
||||
- **Modifications**: Dynamic shadow, dust effects only
|
||||
- **Visual Effects**:
|
||||
- Shadow beneath avatar (scales with elevation)
|
||||
- Launch dust when jumping starts
|
||||
- Landing impact dust when touching ground
|
||||
- Running dust puffs
|
||||
- Circling stars when hurt
|
||||
- Celebratory confetti
|
||||
- **Animations**: idle, walk, run, jump, fall, crouch, hurt, celebrate
|
||||
- **State Effects**:
|
||||
- `running`: Dust puffs (frequency 0.1)
|
||||
- `jumping`: Launch dust (once)
|
||||
- `landing`: Impact dust (once)
|
||||
- `hurt`: Stars around head (duration 0.5s)
|
||||
- `celebrating`: Confetti (duration 3.0s)
|
||||
- **Accessory Visibility**: All visible
|
||||
- **Best For**: Platformers, adventure games, action games
|
||||
- **Recommended Music**: Playful, heroic
|
||||
|
||||
---
|
||||
|
||||
### sports-player
|
||||
|
||||
- **ID**: `sports-player`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Athletic character with minimal sports equipment (jersey number 7)
|
||||
- **Modifications**: Sweatband on forehead, jersey number on back
|
||||
- **Visual Effects**:
|
||||
- Elongated court/field shadow
|
||||
- Speed lines when running fast (threshold 0.7)
|
||||
- Motion blur when jumping at speed
|
||||
- Sparkles when scoring
|
||||
- Impact flash when hurt
|
||||
- **Animations**: idle, walk, run, jump, fall, crouch, hurt, celebrate, throw, catch, kick
|
||||
- **State Effects**:
|
||||
- `running`: Speed lines (threshold 0.7)
|
||||
- `jumping`: Motion blur
|
||||
- `scoring`: Sparkles (duration 1.0s)
|
||||
- `hurt`: Flash (duration 0.3s)
|
||||
- `celebrating`: Sparkles (duration 2.0s)
|
||||
- **Accessory Visibility**: All visible
|
||||
- **Best For**: Sports games, basketball, soccer, tennis, racing
|
||||
- **Recommended Music**: Energetic
|
||||
|
||||
---
|
||||
|
||||
### adventure-hero
|
||||
|
||||
- **ID**: `adventure-hero`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Adventurous character with backpack and utility belt
|
||||
- **Modifications**: Small brown backpack on back, thin utility belt with pouches
|
||||
- **Visual Effects**:
|
||||
- Dynamic shadow based on light source
|
||||
- Subtle heroic glow when idle (pulsing gold)
|
||||
- Determination lines (golden speed lines) when running
|
||||
- Cape-like motion trails when jumping (blue trailing lines)
|
||||
- Discovery sparkle effect
|
||||
- Damage flash when hurt
|
||||
- **Animations**: idle, walk, run, jump, fall, crouch, hurt, celebrate, climb, push, pull
|
||||
- **State Effects**:
|
||||
- `idle`: Glow (continuous)
|
||||
- `running`: Determination lines (continuous)
|
||||
- `jumping`: Cape motion trails (continuous)
|
||||
- `discovering`: Sparkle (duration 1.5s)
|
||||
- `hurt`: Damage flash (duration 0.4s)
|
||||
- **Accessory Visibility**: All visible
|
||||
- **Best For**: Adventure games, puzzle games, exploration, lite RPGs
|
||||
- **Recommended Music**: Adventurous
|
||||
|
||||
---
|
||||
|
||||
### runner
|
||||
|
||||
- **ID**: `runner`
|
||||
- **Mode**: FULL_BODY
|
||||
- **Description**: Streamlined character optimized for endless runner games
|
||||
- **Modifications**: None (pure avatar)
|
||||
- **Visual Effects**:
|
||||
- Motion-blurred shadow (trails behind based on speed)
|
||||
- Constant speed lines (intensity based on speed)
|
||||
- Intensified speed effect when accelerating
|
||||
- Air trail when jumping/falling
|
||||
- Wind-swept hair effect (wind lines from head)
|
||||
- Ground friction sparks when sliding
|
||||
- Pickup sparkle when collecting items
|
||||
- Stumble effect with shake lines when hurt
|
||||
- Stars for big hits
|
||||
- **Avatar Position**: Slightly left (x: 0.4) to show forward space
|
||||
- **Default Animation**: run (not idle)
|
||||
- **Animations**: idle, run, jump, slide, fall, hurt, celebrate
|
||||
- **State Effects**:
|
||||
- `running`: Speed lines (continuous)
|
||||
- `accelerating`: Intensified speed (continuous)
|
||||
- `jumping`: Air trail (continuous)
|
||||
- `sliding`: Sparks (continuous)
|
||||
- `collecting`: Sparkle (duration 0.5s)
|
||||
- `hurt`: Stumble effect (duration 0.6s)
|
||||
- **Accessory Visibility**: All visible
|
||||
- **Best For**: Endless runners, racing, speed games
|
||||
- **Recommended Music**: Fast-paced, electronic
|
||||
|
||||
---
|
||||
|
||||
## Context Selection Guide
|
||||
|
||||
### By Game Type
|
||||
|
||||
| Game Type | Recommended Contexts |
|
||||
|-----------|---------------------|
|
||||
| Space shooter | spaceship-cockpit, mech-suit |
|
||||
| Racing | race-car, motorcycle, hoverboard |
|
||||
| Flying/Flappy | flappy-style, airplane, jetpack, dragon-rider |
|
||||
| Platformer | platformer-standard, adventure-hero, superhero-cape |
|
||||
| Endless Runner | runner, hoverboard, motorcycle |
|
||||
| RPG | knight-armor, wizard-robes, ninja-outfit, adventure-hero |
|
||||
| Sports | athlete-uniform, sports-player |
|
||||
| Puzzle | platformer-standard, scientist-labcoat, wizard-robes |
|
||||
| Cooking | chef-outfit |
|
||||
| Western | cowboy-gear |
|
||||
| Underwater | submarine, space-suit |
|
||||
| Fantasy | dragon-rider, knight-armor, wizard-robes |
|
||||
| Arcade/Retro | pacman-style, flappy-style, spaceship-cockpit |
|
||||
| Military | tank, mech-suit |
|
||||
| Stealth | ninja-outfit |
|
||||
| Pirate/Adventure | pirate-outfit, adventure-hero |
|
||||
|
||||
### By Avatar Mode
|
||||
|
||||
| Mode | Contexts |
|
||||
|------|----------|
|
||||
| HEAD_ONLY | spaceship-cockpit, tank, pacman-style, mech-suit, flappy-style |
|
||||
| HEAD_AND_SHOULDERS | race-car, airplane, submarine |
|
||||
| UPPER_BODY | hoverboard, jetpack |
|
||||
| FULL_BODY | dragon-rider, motorcycle, all costumes (10), all pure (4) |
|
||||
|
||||
### By Category
|
||||
|
||||
| Category | Count | Contexts |
|
||||
|----------|-------|----------|
|
||||
| Vehicle | 12 | spaceship-cockpit, race-car, airplane, flappy-style, tank, pacman-style, hoverboard, jetpack, mech-suit, submarine, dragon-rider, motorcycle |
|
||||
| Costume | 10 | knight-armor, space-suit, ninja-outfit, wizard-robes, superhero-cape, athlete-uniform, pirate-outfit, scientist-labcoat, chef-outfit, cowboy-gear |
|
||||
| Pure | 4 | platformer-standard, sports-player, adventure-hero, runner |
|
||||
|
||||
---
|
||||
|
||||
## API Usage
|
||||
|
||||
### Getting Context Information
|
||||
|
||||
```javascript
|
||||
// Get all contexts
|
||||
const allContexts = ContextCatalog.getAll();
|
||||
|
||||
// Get a specific context by ID
|
||||
const context = ContextCatalog.getById('spaceship-cockpit');
|
||||
|
||||
// Get contexts by category
|
||||
const vehicles = ContextCatalog.getByCategory('vehicle');
|
||||
const costumes = ContextCatalog.getByCategory('costume');
|
||||
const pure = ContextCatalog.getByCategory('pure');
|
||||
|
||||
// Get contexts by avatar mode
|
||||
const headOnly = ContextCatalog.getByMode('HEAD_ONLY');
|
||||
const fullBody = ContextCatalog.getByMode('FULL_BODY');
|
||||
|
||||
// Get recommended contexts for a game type
|
||||
const rpgContexts = ContextCatalog.getRecommendedFor('rpg');
|
||||
const racingContexts = ContextCatalog.getRecommendedFor('racing');
|
||||
|
||||
// Search contexts
|
||||
const matches = ContextCatalog.search('knight');
|
||||
|
||||
// Filter contexts
|
||||
const filtered = ContextCatalog.filter({
|
||||
category: 'costume',
|
||||
mode: 'FULL_BODY',
|
||||
gameType: 'rpg'
|
||||
});
|
||||
|
||||
// Get random context
|
||||
const random = ContextCatalog.getRandom();
|
||||
const randomForType = ContextCatalog.getRandomForGameType('platformer');
|
||||
```
|
||||
|
||||
### Applying Context to Avatar
|
||||
|
||||
```javascript
|
||||
// Apply context to an avatar object
|
||||
const avatarWithContext = ContextRenderer.apply(avatar, 'spaceship-cockpit');
|
||||
|
||||
// Render the avatar with context
|
||||
ContextRenderer.render(avatarWithContext, ctx, x, y, {
|
||||
scale: 1.0,
|
||||
time: elapsedTime
|
||||
});
|
||||
|
||||
// Trigger an animation
|
||||
ContextRenderer.animate(avatarWithContext, 'celebrate');
|
||||
|
||||
// Update state for effects
|
||||
ContextRenderer.setState(avatarWithContext, {
|
||||
running: true,
|
||||
speed: 0.8,
|
||||
facingRight: true
|
||||
});
|
||||
```
|
||||
|
||||
### Context Object Structure
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'context-id',
|
||||
name: 'Human Readable Name',
|
||||
category: 'vehicle' | 'costume' | 'pure',
|
||||
avatarMode: 'HEAD_ONLY' | 'HEAD_AND_SHOULDERS' | 'UPPER_BODY' | 'FULL_BODY',
|
||||
animations: ['idle', 'walk', 'attack', ...],
|
||||
defaultAnimation: 'idle',
|
||||
accessoryVisibility: {
|
||||
eyewear: true | false,
|
||||
headwear: true | false,
|
||||
effects: true | false
|
||||
},
|
||||
layerOrder: ['background', 'avatar', 'foreground'],
|
||||
recommendedFor: ['game-type-1', 'game-type-2'],
|
||||
description: 'Short description',
|
||||
avatarPosition: { x: 0.5, y: 0.5 },
|
||||
avatarScale: 1.0,
|
||||
props: [
|
||||
{ id: 'prop-id', anchor: 'rightArm', offset: { x: 0, y: 0 } }
|
||||
],
|
||||
contextEffects: {
|
||||
effectName: { type: 'effect-type', ... }
|
||||
},
|
||||
renderBackground: function(ctx, width, height, time, options) { ... },
|
||||
renderForeground: function(ctx, width, height, time, options) { ... },
|
||||
renderCostumeUnder: function(ctx, avatar, x, y, scale, transforms, time) { ... },
|
||||
renderCostumeOver: function(ctx, avatar, x, y, scale, transforms, time) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
|
||||
- **Total Contexts**: 26
|
||||
- **Vehicle Contexts**: 12
|
||||
- **Costume Contexts**: 10
|
||||
- **Pure Contexts**: 4
|
||||
- **Version**: 1.0.0
|
||||
- **Last Updated**: 2026-02-05
|
||||
|
||||
---
|
||||
|
||||
## Source Files
|
||||
|
||||
- `/frontend/public/assets/avatar/contextCatalog.js` - Main catalog aggregator
|
||||
- `/frontend/public/assets/avatar/contextRenderer.js` - Rendering engine
|
||||
- `/frontend/public/assets/avatar/contexts/vehicles.js` - Vehicle context definitions (12)
|
||||
- `/frontend/public/assets/avatar/contexts/costumes.js` - Costume context definitions (10)
|
||||
- `/frontend/public/assets/avatar/contexts/pure.js` - Pure context definitions (4)
|
||||
@@ -0,0 +1,170 @@
|
||||
# Game Generation Guide for AI
|
||||
|
||||
This guide helps Haiku (Claude AI) select appropriate assets when generating games.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
### Step 1: Identify Game Style
|
||||
|
||||
Based on user's prompt, classify into one of 11 styles:
|
||||
|
||||
| Keywords | Game Style |
|
||||
|----------|------------|
|
||||
| shoot, space, action, fast, arcade | action-arcade |
|
||||
| puzzle, match, brain, logic | puzzle |
|
||||
| story, adventure, narrative, choice | story-adventure |
|
||||
| race, car, speed, run | racing |
|
||||
| pet, animal, care, farm | pet-simulator |
|
||||
| quiz, trivia, question, knowledge | trivia-quiz |
|
||||
| music, rhythm, dance, beat | music-rhythm |
|
||||
| draw, create, art, design | creative-drawing |
|
||||
| rpg, battle, quest, dungeon | rpg-battle |
|
||||
| sports, ball, soccer, basketball | sports |
|
||||
| physics, pinball, bounce, gravity | physics-pinball |
|
||||
|
||||
### Step 2: Detect Theme
|
||||
|
||||
| Theme Keywords | Background Theme |
|
||||
|----------------|-----------------|
|
||||
| space, alien, star, galaxy | space |
|
||||
| forest, nature, tree, outdoor | nature |
|
||||
| city, street, building, urban | urban |
|
||||
| magic, wizard, dragon, castle | fantasy |
|
||||
| ghost, haunted, scary, dark | spooky |
|
||||
| ocean, fish, underwater, sea | underwater |
|
||||
| retro, pixel, arcade, classic | retro |
|
||||
| sports, stadium, field, court | sports |
|
||||
| sky, flying, clouds, air | sky |
|
||||
| rainbow, colorful, bright, party | colorful |
|
||||
| robot, machine, factory, tech | mechanical |
|
||||
| home, room, kitchen, office | indoor |
|
||||
| rain, snow, storm, weather | weather |
|
||||
| spring, summer, autumn, winter | seasonal |
|
||||
| abstract, minimal, pattern | abstract |
|
||||
|
||||
### Step 3: Detect Mood
|
||||
|
||||
| Mood Keywords | Music Mood |
|
||||
|---------------|------------|
|
||||
| epic, battle, intense, boss | Epic |
|
||||
| calm, relax, peaceful, zen | Chill |
|
||||
| fast, action, rush, chase | Intense |
|
||||
| fun, happy, cute, silly | Playful |
|
||||
| mystery, scary, dark, creepy | Mysterious |
|
||||
| hero, adventure, brave | Heroic |
|
||||
| weird, unique, strange | Quirky |
|
||||
| background, ambient, subtle | Ambient |
|
||||
|
||||
### Step 4: Determine Avatar Mode
|
||||
|
||||
| Game Mechanic | Avatar Mode | Context |
|
||||
|---------------|-------------|---------|
|
||||
| Cockpit/driving view | HEAD_ONLY | spaceship-cockpit, tank, mech-suit |
|
||||
| Driving with body visible | HEAD_AND_SHOULDERS | race-car, airplane, submarine |
|
||||
| Upper body action | UPPER_BODY | hoverboard, jetpack |
|
||||
| Full character movement | FULL_BODY | platformer-standard, costumes |
|
||||
|
||||
### Step 5: Select Context
|
||||
|
||||
| Game Type | Recommended Context |
|
||||
|-----------|---------------------|
|
||||
| Space shooter | spaceship-cockpit |
|
||||
| Racing game | race-car, motorcycle |
|
||||
| Flappy clone | flappy-style |
|
||||
| Tank game | tank |
|
||||
| Maze game (pacman) | pacman-style |
|
||||
| Hoverboard/skateboard | hoverboard |
|
||||
| Jetpack game | jetpack |
|
||||
| Mech combat | mech-suit |
|
||||
| Underwater | submarine |
|
||||
| Dragon game | dragon-rider |
|
||||
| Platformer | platformer-standard |
|
||||
| RPG/Adventure | knight-armor, wizard-robes, ninja-outfit |
|
||||
| Sports | athlete-uniform, sports-player |
|
||||
| Cooking | chef-outfit |
|
||||
| Science | scientist-labcoat |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Presets for Variety
|
||||
```javascript
|
||||
// DON'T: Always use the same combination
|
||||
background: { theme: 'space', variant: 'nebula' }
|
||||
|
||||
// DO: Use different presets or variations
|
||||
const preset = AssetPresets.getRandom('action-arcade');
|
||||
```
|
||||
|
||||
### 2. Match Mood and Theme
|
||||
```javascript
|
||||
// GOOD: Epic music + Space background
|
||||
{ music: { mood: 'Epic' }, background: { theme: 'space' } }
|
||||
|
||||
// BAD: Chill music + Intense action background
|
||||
{ music: { mood: 'Chill' }, background: { theme: 'mechanical' } }
|
||||
```
|
||||
|
||||
### 3. Match Context to Gameplay
|
||||
```javascript
|
||||
// GOOD: Flying game → HEAD_ONLY + flappy-style
|
||||
// GOOD: Platformer → FULL_BODY + platformer-standard
|
||||
// BAD: Racing game → FULL_BODY (should be HEAD_AND_SHOULDERS)
|
||||
```
|
||||
|
||||
### 4. Validate Combinations
|
||||
```javascript
|
||||
const score = AssetCompatibility.validate(config).score;
|
||||
// Aim for score > 70
|
||||
```
|
||||
|
||||
## Example Selections
|
||||
|
||||
### "space shooter with aliens"
|
||||
```javascript
|
||||
{
|
||||
music: { mood: 'Epic', energy: 8 },
|
||||
background: { theme: 'space', variant: 'nebula' },
|
||||
avatarContext: { mode: 'HEAD_ONLY', context: 'spaceship-cockpit' }
|
||||
}
|
||||
```
|
||||
|
||||
### "relaxing puzzle game with nature"
|
||||
```javascript
|
||||
{
|
||||
music: { mood: 'Chill', energy: 3 },
|
||||
background: { theme: 'nature', variant: 'meadow' },
|
||||
avatarContext: { mode: 'FULL_BODY', context: 'platformer-standard' }
|
||||
}
|
||||
```
|
||||
|
||||
### "medieval RPG with dragon battles"
|
||||
```javascript
|
||||
{
|
||||
music: { mood: 'Epic', energy: 9 },
|
||||
background: { theme: 'fantasy', variant: 'castle' },
|
||||
avatarContext: { mode: 'FULL_BODY', context: 'knight-armor' }
|
||||
}
|
||||
```
|
||||
|
||||
### "scary haunted house escape"
|
||||
```javascript
|
||||
{
|
||||
music: { mood: 'Mysterious', energy: 5 },
|
||||
background: { theme: 'spooky', variant: 'hauntedHouse' },
|
||||
avatarContext: { mode: 'FULL_BODY', context: 'adventure-hero' }
|
||||
}
|
||||
```
|
||||
|
||||
## Tracking Variety
|
||||
|
||||
To ensure games don't all look the same:
|
||||
|
||||
1. Rotate through presets (don't always pick index 0)
|
||||
2. Use different background variants within same theme
|
||||
3. Mix costumes even for similar game types
|
||||
4. Vary music energy levels
|
||||
|
||||
## Template Selection
|
||||
|
||||
- Use `gameTemplateAssets.html` when avatar matters
|
||||
- Use `gameTemplateSimple.html` for abstract/puzzle games without avatars
|
||||
@@ -0,0 +1,406 @@
|
||||
# Music Catalog
|
||||
|
||||
## Overview
|
||||
|
||||
160 procedural songs generated using Tone.js.
|
||||
8 moods x 20 songs each.
|
||||
|
||||
All songs are synthesized in real-time using the MusicEngine, which reads song definitions from MusicLibrary and generates audio procedurally using Tone.js synths, effects, and chord progressions.
|
||||
|
||||
---
|
||||
|
||||
## Mood Categories
|
||||
|
||||
### Epic (20 songs)
|
||||
|
||||
Energy range: 7-10
|
||||
Best for: Boss battles, climactic moments, action sequences
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| epic_001 | Siege of Thunder | 9 | 150 | C minor | action, siege, storm | Thunderous orchestral assault with relentless percussion and soaring brass lines |
|
||||
| epic_002 | Dragon's Wrath | 10 | 160 | D harmonic minor | dragon, boss-fight, powerful | Ferocious harmonic minor shred with explosive drum fills and scorching lead |
|
||||
| epic_003 | Kingdom's Fall | 8 | 135 | E dorian | kingdom, dramatic, war | Brooding dorian march that builds from sorrowful strings to a devastating climax |
|
||||
| epic_004 | Titan Ascendant | 9 | 145 | F minor | titan, heroic, climax | Ascending power theme with massive layered synths and triumphant resolution |
|
||||
| epic_005 | Forge of Legends | 8 | 130 | G melodic minor | forge, battle, powerful | Heavy melodic minor groove with hammering rhythms and a blazing lead melody |
|
||||
| epic_006 | Storm Legion | 9 | 155 | A harmonic minor | storm, legion, fierce | Rapid harmonic minor fury with militant snare rolls and piercing lead runs |
|
||||
| epic_007 | Conquest of Dawn | 8 | 140 | B minor | conquest, heroic, victory | Majestic dawn-rise theme with layered pads building to a grand fanfare |
|
||||
| epic_008 | Blood of the Arena | 10 | 165 | C# phrygian | battle, fierce, combat | Phrygian gladiator anthem with thundering kicks and savage melodic hooks |
|
||||
| epic_009 | Throne Eternal | 7 | 125 | D# dorian | throne, dramatic, kingdom | Stately dorian processional with regal brass swells and ceremonial percussion |
|
||||
| epic_010 | Wings of Valkyrie | 9 | 152 | F# harmonic minor | heroic, action, victory | Soaring harmonic minor flight with rapid arpeggios and a euphoric peak |
|
||||
| epic_011 | The Iron March | 8 | 132 | G# minor | war, legion, march | Crushing military march with iron-clad percussion and a resolute melodic theme |
|
||||
| epic_012 | Celestial Siege | 9 | 148 | A# melodic minor | siege, climax, powerful | Heavenly melodic minor ascent colliding with earth-shaking low end and choir-like pads |
|
||||
| epic_013 | Oathbreaker | 10 | 170 | E harmonic minor | boss-fight, aggressive, dramatic | Blistering boss battle anthem with relentless double-kicks and a searing harmonic minor riff |
|
||||
| epic_014 | Pillars of Eternity | 7 | 120 | F dorian | dramatic, kingdom, throne | Grand dorian hymn with towering pad layers and a slow-building, inevitable crescendo |
|
||||
| epic_015 | The Last Bastion | 9 | 142 | A minor | siege, heroic, battle | Desperate last-stand theme with urgent strings and a defiant, rising chorus |
|
||||
| epic_016 | Warcry of the North | 10 | 158 | B harmonic minor | war, fierce, conquest | Nordic war chant with thunderous toms, harsh harmonic minor runs, and tribal energy |
|
||||
| epic_017 | Shattered Crown | 8 | 138 | C# dorian | dramatic, throne, powerful | Regal dorian tragedy with broken chord progressions that rebuild into glory |
|
||||
| epic_018 | Dragonfire Overture | 9 | 162 | D melodic minor | dragon, action, climax | Scorching melodic minor overture with blazing fast arpeggios and massive drops |
|
||||
| epic_019 | Echoes of Valhalla | 8 | 128 | G minor | victory, heroic, legend | Heroic afterglow theme with echoing delays, reverent pads, and a noble melody |
|
||||
| epic_020 | Apex Predator | 10 | 168 | D# phrygian | boss-fight, titan, aggressive | Phrygian apex boss theme with crushing low end, frantic leads, and zero mercy |
|
||||
|
||||
---
|
||||
|
||||
### Chill (20 songs)
|
||||
|
||||
Energy range: 1-4
|
||||
Best for: Puzzle games, menus, relaxation
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| chill_001 | Sunset Horizon | 2 | 80 | C major | sunset, calm, peaceful | Warm golden-hour pads with gentle plucked melodies drifting over a soft beat |
|
||||
| chill_002 | Cloud Nine | 2 | 85 | D lydian | cloud, zen, soft | Floating lydian dreamscape with airy pads and a delicate crystalline lead |
|
||||
| chill_003 | Gentle Stream | 1 | 72 | F major | stream, peaceful, meditation | Babbling brook ambience with minimal plucked notes and vast reverb spaces |
|
||||
| chill_004 | Amber Dusk | 3 | 90 | G mixolydian | sunset, smooth, lounge | Laid-back mixolydian groove with a silky lead line and soft shuffled percussion |
|
||||
| chill_005 | Velvet Rain | 2 | 78 | A dorian | rain, calm, soft | Tender dorian rainfall with muted plucks and warm sub-bass undertones |
|
||||
| chill_006 | Morning Dew | 3 | 95 | E pentatonic major | breeze, garden, easy | Fresh pentatonic morning with sparkling plucks and a gentle rhythmic pulse |
|
||||
| chill_007 | Sapphire Coast | 3 | 92 | B major | shore, smooth, relax | Coastal shimmer with wave-like pad swells and a melodic bass walk |
|
||||
| chill_008 | Paper Lanterns | 2 | 76 | D# lydian | zen, peaceful, meditation | Floating lydian lantern-light with shimmering overtones and barely-there percussion |
|
||||
| chill_009 | Lavender Fields | 2 | 82 | F# major | garden, calm, soft | Pastoral meadow soundscape with singing pads and tiny melodic blossoms |
|
||||
| chill_010 | Moonlit Canopy | 1 | 70 | C# dorian | zen, meditation, soft | Deep forest night ambience with owl-like calls and a hypnotic dorian drone |
|
||||
| chill_011 | Drifting Kites | 3 | 100 | G# pentatonic major | breeze, cheerful, easy | Breezy pentatonic float with a playful pluck melody and airy pad washes |
|
||||
| chill_012 | Still Waters | 1 | 74 | A# major | calm, peaceful, meditation | Mirror-flat lake ambience with slow reverb tails and barely audible melodic whispers |
|
||||
| chill_013 | Silken Thread | 3 | 96 | D mixolydian | smooth, lounge, easy | Sophisticated mixolydian lounge with buttery chords and a cool walking bass |
|
||||
| chill_014 | Porcelain Sky | 2 | 84 | E lydian | cloud, zen, peaceful | Delicate lydian sky with bell-like plucks floating over an endless pad horizon |
|
||||
| chill_015 | Terracotta Noon | 4 | 105 | A dorian | smooth, lounge, relax | Sun-baked dorian groove with a loose beat, mellow keys, and an easy bass line |
|
||||
| chill_016 | Snowfall Waltz | 2 | 88 | F major | soft, calm, peaceful | Gentle snowfall waltz with twinkling bells and a feather-light three-beat feel |
|
||||
| chill_017 | Jasmine Tea | 2 | 78 | G pentatonic major | zen, meditation, soft | Meditative pentatonic ritual with ceramic-toned plucks and warm bass hum |
|
||||
| chill_018 | Indigo Hour | 3 | 98 | B dorian | smooth, lounge, sunset | Deep dorian twilight groove with a muted lead, lush pads, and subtle swing |
|
||||
| chill_019 | Floating Feather | 1 | 70 | C lydian | cloud, peaceful, meditation | Weightless lydian drift with overtone-rich pads and the faintest melodic outline |
|
||||
| chill_020 | Harbor Lights | 4 | 110 | F# mixolydian | shore, smooth, relax | Warm harbor-side mixolydian jam with a steady pulse and reflective melodic phrases |
|
||||
|
||||
---
|
||||
|
||||
### Intense (20 songs)
|
||||
|
||||
Energy range: 8-10
|
||||
Best for: Racing, action, time pressure
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| intense_001 | Midnight Chase | 9 | 160 | C minor | chase, urgent, adrenaline | Heart-pounding minor key pursuit with relentless four-on-floor and staccato stabs |
|
||||
| intense_002 | Fury Unleashed | 10 | 175 | D phrygian | fury, aggressive, rampage | Phrygian berserker rage with distorted bass, frantic leads, and pummeling kicks |
|
||||
| intense_003 | Overdrive | 9 | 155 | E minor | overdrive, fast, thrilling | Redlined engine energy with screaming synths and a breakneck tempo that never lets up |
|
||||
| intense_004 | Razor Edge | 10 | 170 | F harmonic minor | danger, aggressive, combat | Razor-sharp harmonic minor slashes with searing leads and machine-gun percussion |
|
||||
| intense_005 | Panic Protocol | 9 | 165 | G minor | panic, urgent, pursuit | Emergency klaxon energy with alarm-like leads and a propulsive bass engine |
|
||||
| intense_006 | Blitz Reaper | 10 | 180 | A phrygian | blitz, fierce, frenzy | Maximum-speed phrygian onslaught with unrelenting double-time drums and savage riffs |
|
||||
| intense_007 | Neon Inferno | 9 | 150 | B blues | adrenaline, thrilling, fast | Blues-infused neon hellscape with gritty lead bends and a stomping groove |
|
||||
| intense_008 | Shockwave | 10 | 172 | C# harmonic minor | combat, aggressive, fierce | Explosive harmonic minor shockblast with staccato stabs and obliterating drops |
|
||||
| intense_009 | Terminal Velocity | 9 | 162 | D# minor | fast, pursuit, adrenaline | Free-falling velocity rush with whipping arpeggios and an unstoppable beat |
|
||||
| intense_010 | Venom Strike | 10 | 168 | F# phrygian | danger, combat, rampage | Venomous phrygian attack with serpentine lead phrases and toxic bass drops |
|
||||
| intense_011 | Wraith Runner | 9 | 156 | G# harmonic minor | chase, thrilling, urgent | Ghostly harmonic minor sprint with phasing leads and a thundering rhythmic foundation |
|
||||
| intense_012 | Meltdown | 10 | 176 | A# chromatic | frenzy, aggressive, panic | Nuclear meltdown chaos with chromatic madness and total percussive devastation |
|
||||
| intense_013 | Iron Lung | 8 | 148 | C blues | fierce, thrilling, overdrive | Breathless blues-scale assault with a heavy swing and gritty overdriven bass |
|
||||
| intense_014 | Voltage Surge | 9 | 158 | D minor | adrenaline, fast, urgent | Electric voltage spike with buzzing FM leads and an unstoppable rhythmic charge |
|
||||
| intense_015 | Reckless Descent | 9 | 152 | E harmonic minor | danger, thrilling, pursuit | Spiraling harmonic minor descent with cascading leads and hammering kicks |
|
||||
| intense_016 | Firestorm Protocol | 10 | 174 | F phrygian | combat, aggressive, blitz | Scorched-earth phrygian devastation with relentless snare rolls and wall-of-sound bass |
|
||||
| intense_017 | Cascade Fury | 8 | 145 | G minor | fierce, pursuit, thrilling | Cascading minor fury with tumbling melodic runs and a rock-solid groove foundation |
|
||||
| intense_018 | Deathline Express | 10 | 178 | A harmonic minor | frenzy, rampage, aggressive | Runaway-train harmonic minor express with breakneck speed and zero stopping power |
|
||||
| intense_019 | Concrete Jungle | 8 | 142 | B blues | urgent, adrenaline, thrilling | Gritty urban blues assault with dirty bass growls and relentless breakbeat energy |
|
||||
| intense_020 | Event Horizon | 9 | 164 | D# chromatic | danger, panic, fast | Reality-warping chromatic vortex pulling everything into a relentless rhythmic singularity |
|
||||
|
||||
---
|
||||
|
||||
### Playful (20 songs)
|
||||
|
||||
Energy range: 4-7
|
||||
Best for: Kids games, casual games, fun moments
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| playful_001 | Bubble Pop | 6 | 130 | C major | fun, bouncy, bubble | Bouncy pop confection with fizzy pluck melodies and a toe-tapping beat |
|
||||
| playful_002 | Candy Rush | 7 | 140 | D mixolydian | candy, happy, party | Sugar-coated mixolydian romp with rapid-fire melodies and a party-floor beat |
|
||||
| playful_003 | Skip & Jump | 5 | 120 | F major | skip, cheerful, lighthearted | Carefree skip-along with a lilting melody, simple chords, and a playful shuffle |
|
||||
| playful_004 | Sunshine Express | 6 | 135 | G lydian | sunshine, happy, dance | Bright lydian sunshine ride with sparkling chords and an infectious rhythmic hook |
|
||||
| playful_005 | Frog Hop | 5 | 118 | A pentatonic major | jump, silly, cartoon | Quirky pentatonic hop with bouncing bass notes and a whimsical plucked melody |
|
||||
| playful_006 | Pixel Parade | 6 | 132 | E major | fun, cheerful, party | Retro pixel-art parade with chiptuney leads and a marching festival vibe |
|
||||
| playful_007 | Jellybean Bounce | 7 | 142 | B mixolydian | bouncy, candy, dance | Maximum-bounce mixolydian groove with rubbery bass and a sugary melodic hook |
|
||||
| playful_008 | Rainbow Sprinkles | 5 | 125 | C# lydian | rainbow, happy, lighthearted | Colorful lydian celebration with chiming leads and a gentle, flowing groove |
|
||||
| playful_009 | Trampoline | 7 | 145 | D# major | jump, bouncy, fun | Sky-high trampoline energy with elastic bass and soaring staccato rebounds |
|
||||
| playful_010 | Whirlwind Waltz | 4 | 115 | F# major | dance, cheerful, lighthearted | Gentle whirlwind waltz with graceful plucks and a charming three-beat lilt |
|
||||
| playful_011 | Firefly Festival | 5 | 122 | G# pentatonic major | cheerful, fun, frolic | Twinkling firefly night with dancing pentatonic melodies and a warm, steady pulse |
|
||||
| playful_012 | Lollipop Lane | 6 | 128 | A# major | candy, silly, cartoon | Sweet lollipop stroll with cartoon-like sound effects and a skip-worthy rhythm |
|
||||
| playful_013 | Dizzy Spin | 7 | 148 | E dorian | fun, dance, party | Dizzying dorian spin cycle with whirling arpeggios and a head-bobbing groove |
|
||||
| playful_014 | Cotton Candy Clouds | 4 | 112 | F lydian | cloud, happy, lighthearted | Fluffy lydian cotton-candy float with soft plucks and dreamy pad wisps |
|
||||
| playful_015 | Pinball Wizard | 7 | 150 | A mixolydian | fun, bouncy, party | Rapid-fire pinball ricochet with dinging FM leads and a hyperactive bassline |
|
||||
| playful_016 | Confetti Cannon | 6 | 138 | B major | party, cheerful, dance | Explosive confetti burst with bright staccato hits and a celebratory chorus |
|
||||
| playful_017 | Rubber Duck | 5 | 116 | D pentatonic major | silly, cartoon, fun | Squeaky-clean pentatonic ditty with rubbery bass plucks and a goofy swing |
|
||||
| playful_018 | Kite Runner | 5 | 124 | G dorian | frolic, lighthearted, breeze | Breezy dorian kite flight with soaring lead phrases and a playful rhythmic tug |
|
||||
| playful_019 | Pancake Flip | 6 | 134 | C mixolydian | silly, fun, bouncy | Morning-kitchen mixolydian jam with flippy rhythms and a warm, buttery tone |
|
||||
| playful_020 | Starlight Carousel | 4 | 110 | F# lydian | dance, happy, lighthearted | Enchanted carousel spin with twinkling lydian bells and a gentle, revolving rhythm |
|
||||
|
||||
---
|
||||
|
||||
### Mysterious (20 songs)
|
||||
|
||||
Energy range: 2-5
|
||||
Best for: Horror, exploration, puzzles
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| mysterious_001 | Shadow Waltz | 3 | 95 | D phrygian | mystery, dark, suspense | Eerie waltz with unsettling chromatic turns and ghostly reverberations |
|
||||
| mysterious_002 | Phantom's Riddle | 4 | 105 | A harmonic minor | phantom, riddle, enigma | Brooding harmonic minor theme with restless melodic questions |
|
||||
| mysterious_003 | Twilight Labyrinth | 3 | 88 | F# minor | twilight, labyrinth, shadow | Winding minor key passages through dim corridors of sound |
|
||||
| mysterious_004 | Crypt Whispers | 2 | 82 | B phrygian | crypt, whisper, eerie | Hushed phrygian murmurs echoing through ancient stone chambers |
|
||||
| mysterious_005 | Fog of Enigma | 3 | 100 | E whole tone | fog, enigma, mystery | Drifting whole-tone haze obscuring all sense of harmonic direction |
|
||||
| mysterious_006 | Spell Unraveled | 5 | 115 | G harmonic minor | spell, dark, haunt | Urgent harmonic minor incantation building to a feverish climax |
|
||||
| mysterious_007 | Void Gazer | 2 | 84 | C chromatic | void, dark, eerie | Staring into the abyss with atonal fragments dissolving into silence |
|
||||
| mysterious_008 | Detective Noir | 4 | 108 | C# minor | detective, suspense, shadow | Smoky noir investigation theme with sly chromatic bass lines |
|
||||
| mysterious_009 | Haunted Carousel | 4 | 112 | F phrygian | haunt, eerie, puzzle | A broken carousel spinning in phrygian darkness with warped melodies |
|
||||
| mysterious_010 | Riddle of Echoes | 3 | 92 | D# harmonic minor | riddle, enigma, whisper | Haunting call-and-response between shadowy melodic fragments |
|
||||
| mysterious_011 | Obsidian Mirror | 3 | 98 | G# minor | dark, mystery, shadow | Dark reflections shimmer in minor key with glassy sustained tones |
|
||||
| mysterious_012 | Serpent Passage | 5 | 118 | A# phrygian | suspense, dark, labyrinth | Slithering phrygian riff coiling through an underground labyrinth |
|
||||
| mysterious_013 | Moonless Night | 2 | 80 | E whole tone | eerie, void, twilight | Formless whole-tone drifts under a starless sky with no resolution |
|
||||
| mysterious_014 | Specter's Descent | 4 | 104 | B harmonic minor | phantom, dark, haunt | Descending harmonic minor spiral pulling deeper into spectral depths |
|
||||
| mysterious_015 | Puzzle Chamber | 4 | 110 | F minor | puzzle, mystery, riddle | Interlocking melodic puzzles clicking into place within stone walls |
|
||||
| mysterious_016 | Veiled Threshold | 3 | 90 | C phrygian | mystery, fog, twilight | Standing at the boundary between worlds in thick phrygian fog |
|
||||
| mysterious_017 | Waning Sigil | 3 | 96 | D harmonic minor | spell, enigma, crypt | A fading magical sigil pulsing with diminishing harmonic minor energy |
|
||||
| mysterious_018 | Forgotten Archive | 3 | 94 | G# chromatic | mystery, puzzle, shadow | Dusty chromatic fragments unearthed from a lost archive of secrets |
|
||||
| mysterious_019 | Oracle's Warning | 5 | 120 | A minor | suspense, dark, whisper | An urgent prophetic warning delivered in frantic minor key passages |
|
||||
| mysterious_020 | Abyssal Lullaby | 2 | 85 | F# whole tone | void, eerie, twilight | A gentle yet deeply unsettling lullaby drifting over an endless abyss |
|
||||
|
||||
---
|
||||
|
||||
### Heroic (20 songs)
|
||||
|
||||
Energy range: 6-9
|
||||
Best for: Adventure, achievements, victories
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| heroic_001 | Champion's Anthem | 8 | 145 | C major | hero, triumph, anthem | A soaring major key anthem celebrating the ultimate champion |
|
||||
| heroic_002 | Dawn of Valor | 7 | 138 | G lydian | dawn, valor, courage | Radiant lydian sunrise heralding a new era of bravery and valor |
|
||||
| heroic_003 | Shield Bearer | 7 | 132 | D mixolydian | shield, brave, quest | Sturdy mixolydian march of a steadfast shield bearer on a noble quest |
|
||||
| heroic_004 | Glory Unchained | 9 | 155 | A major | glory, triumph, champion | Explosive high-energy celebration of total victory and unchained glory |
|
||||
| heroic_005 | Oath of the Paladin | 7 | 128 | F dorian | oath, paladin, courage | Solemn yet powerful dorian oath sworn by a noble paladin |
|
||||
| heroic_006 | Crusader's March | 8 | 142 | E major | crusade, hero, sword | Relentless marching rhythm driving a crusade forward to victory |
|
||||
| heroic_007 | Beacon of Hope | 6 | 125 | B lydian | beacon, hero, legend | A luminous lydian melody shining as a beacon through the darkness |
|
||||
| heroic_008 | Thunder Vanguard | 9 | 158 | D# minor | brave, quest, sword | Thunderous minor key charge of the vanguard into the heart of battle |
|
||||
| heroic_009 | Skyward Bound | 7 | 135 | F# major | adventure, glory, dawn | An uplifting ascent into the clouds with boundless major key optimism |
|
||||
| heroic_010 | Iron Resolve | 8 | 148 | C# dorian | champion, shield, valor | Unbreakable dorian determination with a hammering rhythmic backbone |
|
||||
| heroic_011 | Wingspan | 6 | 122 | G# lydian | adventure, hero, dawn | Soaring lydian theme evoking the spread of mighty wings at first light |
|
||||
| heroic_012 | Legend's Forge | 8 | 150 | A# minor | legend, sword, crusade | White-hot minor key intensity of a legendary weapon being forged |
|
||||
| heroic_013 | Rally Cry | 9 | 160 | D mixolydian | triumph, anthem, glory | A deafening mixolydian rally cry uniting an army before the final charge |
|
||||
| heroic_014 | Valor Rising | 7 | 130 | F major | valor, brave, quest | A steadily rising major key theme embodying growing courage |
|
||||
| heroic_015 | Fortress Eternal | 7 | 136 | B dorian | shield, paladin, oath | Impenetrable dorian fortress theme with layered harmonic defenses |
|
||||
| heroic_016 | Phoenix's Flight | 8 | 146 | E lydian | hero, legend, dawn | Blazing lydian ascent of a reborn phoenix soaring above the flames |
|
||||
| heroic_017 | Banner Unfurled | 7 | 134 | G major | glory, courage, champion | Majestic unfurling of a victorious banner in crisp major key splendor |
|
||||
| heroic_018 | Dragonslayer | 9 | 156 | C# minor | sword, brave, quest | Ferocious minor key battle theme of a legendary dragonslayer |
|
||||
| heroic_019 | Crowning Moment | 6 | 120 | A# major | triumph, anthem, glory | A stately coronation theme building to an emotional crowning climax |
|
||||
| heroic_020 | Eternal Vow | 6 | 126 | F# mixolydian | oath, paladin, beacon | A profound mixolydian pledge of eternal service and unwavering loyalty |
|
||||
|
||||
---
|
||||
|
||||
### Quirky (20 songs)
|
||||
|
||||
Energy range: 5-8
|
||||
Best for: Comedy, unique games, surprises
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| quirky_001 | Wobble Walk | 6 | 118 | C mixolydian | wobble, funny, offbeat | A tipsy mixolydian stroll with syncopated bass and bouncy staccato hits |
|
||||
| quirky_002 | Glitch Parade | 7 | 128 | F blues | glitch, weird, surprise | A malfunctioning parade of blues riffs and unexpected rhythmic hiccups |
|
||||
| quirky_003 | Mischief Maker | 6 | 112 | D dorian | mischief, prank, eccentric | Sneaky dorian theme of a trickster plotting delightful chaos |
|
||||
| quirky_004 | Rubber Duck Boogie | 7 | 132 | A pentatonic minor | funny, bounce, comedy | Absurd pentatonic boogie with squeaky lead lines and rubber bass |
|
||||
| quirky_005 | Clockwork Confusion | 5 | 108 | G whole tone | odd, twist, eccentric | Malfunctioning clockwork mechanism ticking in whole-tone disorientation |
|
||||
| quirky_006 | Prankster Polka | 7 | 135 | E mixolydian | prank, wacky, clown | A manic polka-inspired romp driven by wacky syncopation and flat-seven swagger |
|
||||
| quirky_007 | Jellybean Jamboree | 8 | 140 | B lydian | bounce, zany, comedy | Sugary lydian explosion of color and bouncing melodic candy |
|
||||
| quirky_008 | Sneaky Squirrel | 5 | 105 | D# dorian | mischief, trick, offbeat | Furtive dorian scurrying with darting staccato leads and syncopated bass |
|
||||
| quirky_009 | Topsy Turvy | 6 | 120 | F# blues | chaos, twist, wacky | Everything flipped upside down in a chaotic blues-scale adventure |
|
||||
| quirky_010 | Noodle Soup | 5 | 102 | C# pentatonic minor | weird, odd, eccentric | A winding noodly pentatonic melody sloshing around in rhythmic broth |
|
||||
| quirky_011 | Pixel Pandemonium | 8 | 138 | G# mixolydian | glitch, chaos, surprise | Hyper 8-bit style mixolydian mayhem with frantic arpeggio bursts |
|
||||
| quirky_012 | Banana Peel Slide | 6 | 116 | A# blues | funny, clown, comedy | Slippery blues-scale comedy with pratfall rhythms and slide-whistle leads |
|
||||
| quirky_013 | Funhouse Mirror | 5 | 110 | E whole tone | twist, odd, eccentric | Distorted whole-tone reflections warping reality in a carnival funhouse |
|
||||
| quirky_014 | Hiccup Hop | 7 | 126 | D dorian | bounce, wacky, zany | Jerky dorian hop interrupted by melodic hiccups and rhythmic stumbles |
|
||||
| quirky_015 | Zigzag Zephyr | 6 | 122 | B lydian | surprise, offbeat, mischief | Unpredictable lydian wind gusts pushing melodies in zigzag directions |
|
||||
| quirky_016 | Rubber Band Riff | 6 | 114 | F pentatonic minor | wobble, trick, comedy | Stretchy pentatonic riffs snapping back and forth with elastic energy |
|
||||
| quirky_017 | Carousel Malfunction | 7 | 130 | G mixolydian | chaos, glitch, wacky | A malfunctioning fairground ride spinning at odd intervals in mixolydian frenzy |
|
||||
| quirky_018 | Jester Jig | 7 | 136 | A dorian | clown, prank, zany | A flamboyant dorian jig performed by a mischievous court jester |
|
||||
| quirky_019 | Slapstick Serenade | 5 | 100 | C# blues | funny, comedy, twist | A tender serenade gone hilariously wrong with slapstick timing and blue notes |
|
||||
| quirky_020 | Kaleidoscope Caper | 6 | 124 | D# lydian | surprise, eccentric, wobble | Shifting lydian patterns like a kaleidoscope of musical fragments |
|
||||
|
||||
---
|
||||
|
||||
### Ambient (20 songs)
|
||||
|
||||
Energy range: 1-3
|
||||
Best for: Background, meditation, creative tools
|
||||
|
||||
| ID | Name | Energy | Tempo | Key | Tags | Description |
|
||||
|----|------|--------|-------|-----|------|-------------|
|
||||
| ambient_001 | Nebula Drift | 2 | 68 | C major | space, cosmic, floating | Weightless major key pads drifting through a colorful cosmic nebula |
|
||||
| ambient_002 | Crystal Meadow | 2 | 72 | G pentatonic major | crystal, meadow, tranquil | Sparkling pentatonic chimes scattered across a sunlit meadow |
|
||||
| ambient_003 | Glacial Horizon | 1 | 62 | D lydian | glacier, vast, ethereal | Immense glacial soundscape stretching to an infinite lydian horizon |
|
||||
| ambient_004 | Aurora Veil | 2 | 75 | A pentatonic major | aurora, ethereal, dream | Shimmering aurora borealis painted in pentatonic washes of light |
|
||||
| ambient_005 | Tidal Murmur | 2 | 66 | F minor | tide, mist, tranquil | Gentle minor key tides lapping rhythmically in a misty coastal haze |
|
||||
| ambient_006 | Stardust Canopy | 1 | 60 | B major | stardust, cosmic, vast | An endless canopy of stars rendered in slow-moving major key pads |
|
||||
| ambient_007 | Silk River | 2 | 70 | E pentatonic minor | floating, dream, tranquil | Smooth pentatonic minor currents flowing like liquid silk through space |
|
||||
| ambient_008 | Morning Dew | 2 | 76 | D# lydian | meadow, crystal, atmosphere | Dewdrops catching first light in shimmering lydian bell tones |
|
||||
| ambient_009 | Deep Cavern Echo | 1 | 64 | F# minor | void, vast, atmosphere | Cavernous minor key echoes bouncing endlessly through underground spaces |
|
||||
| ambient_010 | Velvet Cosmos | 2 | 74 | C# whole tone | cosmic, nebula, floating | Luxurious whole-tone textures unfolding across a velvet cosmic canvas |
|
||||
| ambient_011 | Feather Fall | 1 | 63 | G# pentatonic major | ethereal, dream, floating | Weightless pentatonic notes falling like feathers through still air |
|
||||
| ambient_012 | Horizon Glow | 3 | 82 | A# major | horizon, aurora, tranquil | Warm major key glow spreading across a peaceful distant horizon |
|
||||
| ambient_013 | Moss Garden | 2 | 69 | D pentatonic minor | meadow, mist, tranquil | A damp moss garden where pentatonic tones grow like delicate plants |
|
||||
| ambient_014 | Solar Wind | 2 | 78 | F lydian | space, cosmic, vast | Streams of lydian particles carried by an invisible solar wind |
|
||||
| ambient_015 | Moonpool Reflection | 1 | 61 | B minor | dream, mist, ethereal | Perfectly still moonlit pool reflecting minor key pads in mirrored silence |
|
||||
| ambient_016 | Ember Glow | 3 | 85 | E pentatonic major | atmosphere, tranquil, meadow | Warm dying embers casting a pentatonic glow across a quiet room |
|
||||
| ambient_017 | Cloud Temple | 2 | 71 | A lydian | floating, ethereal, vast | An ancient temple floating among the clouds in shimmering lydian suspension |
|
||||
| ambient_018 | Frozen Bloom | 1 | 65 | G whole tone | glacier, crystal, void | Ice crystals blooming in slow motion with directionless whole-tone shimmer |
|
||||
| ambient_019 | Whispering Sands | 2 | 73 | C# pentatonic minor | tide, mist, atmosphere | Desert sands whispering pentatonic secrets carried on a warm breeze |
|
||||
| ambient_020 | Infinite Stillness | 1 | 60 | F# major | void, stardust, vast | Absolute stillness at the edge of the universe where sound barely exists |
|
||||
|
||||
---
|
||||
|
||||
## Compatibility Matrix
|
||||
|
||||
### Mood to Background Theme
|
||||
|
||||
| Mood | Excellent Themes | Good Themes | Avoid |
|
||||
|------|-----------------|-------------|-------|
|
||||
| Epic | space, fantasy, dungeon, volcanic | urban, stadium, castle | pastel, garden, beach |
|
||||
| Chill | garden, beach, sunset, meadow | forest, sky, underwater | dungeon, volcanic, industrial |
|
||||
| Intense | urban, industrial, volcanic, stadium | space, dungeon, desert | garden, beach, meadow |
|
||||
| Playful | candy, circus, playground, cartoon | beach, forest, sky | dungeon, volcanic, horror |
|
||||
| Mysterious | dungeon, forest-night, underwater-deep, ancient-ruins | castle, space-dark, swamp | candy, playground, beach |
|
||||
| Heroic | castle, stadium, mountain-peak, sky | fantasy, forest, space | dungeon, horror, swamp |
|
||||
| Quirky | circus, cartoon, laboratory, toy-room | urban, candy, beach | dungeon, horror, volcanic |
|
||||
| Ambient | space, underwater, forest, sky | desert, beach, meadow | urban, stadium, industrial |
|
||||
|
||||
### Mood to Avatar Context
|
||||
|
||||
| Mood | Recommended Contexts |
|
||||
|------|---------------------|
|
||||
| Epic | knight-armor, spaceship-cockpit, dragon-rider, warrior-arena, commander-bridge |
|
||||
| Chill | beach-chair, hammock, cafe-window, meditation-cushion, garden-bench |
|
||||
| Intense | race-car, fighter-jet, motorcycle, battle-station, control-room |
|
||||
| Playful | playground, trampoline, carnival-booth, ball-pit, treehouse |
|
||||
| Mysterious | detective-office, ancient-library, foggy-alley, crystal-cave, haunted-mansion |
|
||||
| Heroic | throne-room, mountain-summit, victory-podium, ship-helm, castle-balcony |
|
||||
| Quirky | mad-scientist-lab, upside-down-room, funhouse, time-machine, cartoon-world |
|
||||
| Ambient | starship-window, underwater-dome, cloud-platform, zen-garden, space-station |
|
||||
|
||||
---
|
||||
|
||||
## API Usage
|
||||
|
||||
```javascript
|
||||
// Initialize (called automatically on first play)
|
||||
await MusicEngine.init();
|
||||
|
||||
// Play random song from mood
|
||||
const song = await MusicEngine.playRandomSong('Epic');
|
||||
console.log(song.name); // e.g., "Siege of Thunder"
|
||||
|
||||
// Play specific song by ID
|
||||
const song = await MusicEngine.playSong('epic_001');
|
||||
|
||||
// Find songs by criteria
|
||||
const songs = MusicEngine.findSongs({
|
||||
mood: 'Epic',
|
||||
minEnergy: 8,
|
||||
maxEnergy: 10,
|
||||
minTempo: 150,
|
||||
maxTempo: 180,
|
||||
tags: ['battle']
|
||||
});
|
||||
|
||||
// Get full catalog with metadata
|
||||
const allSongs = MusicEngine.getCatalog();
|
||||
// Returns: [{ id, name, mood, energy, tempo, key, scale, tags, desc }, ...]
|
||||
|
||||
// Volume control (0.0 to 1.0)
|
||||
MusicEngine.setVolume(0.7);
|
||||
|
||||
// Fade out current song (duration in seconds)
|
||||
MusicEngine.fadeOut(2);
|
||||
|
||||
// Stop music immediately
|
||||
MusicEngine.stopMusic();
|
||||
|
||||
// Check current state
|
||||
const isPlaying = MusicEngine.isPlaying();
|
||||
const currentSong = MusicEngine.getCurrentSong();
|
||||
|
||||
// Get all available moods
|
||||
const moods = MusicLibrary.getMoods();
|
||||
// Returns: ['Epic', 'Chill', 'Intense', 'Playful', 'Mysterious', 'Heroic', 'Quirky', 'Ambient']
|
||||
|
||||
// Get songs for a specific mood
|
||||
const epicSongs = MusicLibrary.getSongsByMood('Epic');
|
||||
|
||||
// Get a specific song definition
|
||||
const song = MusicLibrary.getSong('epic_001');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Synthesis
|
||||
|
||||
- **Engine**: Tone.js (loaded from CDN)
|
||||
- **Duration**: 60+ seconds per song (loops seamlessly)
|
||||
- **Sample Rate**: 44.1 kHz (browser default)
|
||||
|
||||
### Synth Types Used
|
||||
|
||||
| Type | Tone.js Class | Typical Use |
|
||||
|------|---------------|-------------|
|
||||
| FM | FMSynth | Bright leads, stabs, bells |
|
||||
| AM | AMSynth | Warm pads, subtle leads |
|
||||
| Mono | MonoSynth | Bass, monophonic leads |
|
||||
| Poly | PolySynth | Pads, chords |
|
||||
| Membrane | MembraneSynth | Drums, percussive bass |
|
||||
| Metal | MetalSynth | Metallic percussion |
|
||||
| Pluck | PluckSynth | Plucked strings, mallets |
|
||||
| Duo | DuoSynth | Rich leads, doubled tones |
|
||||
| Noise | NoiseSynth | Effects, ambience |
|
||||
|
||||
### Effects Chain
|
||||
|
||||
| Effect | Parameter | Range | Purpose |
|
||||
|--------|-----------|-------|---------|
|
||||
| Reverb | reverb | 0.1-0.8 | Room size/tail |
|
||||
| Delay | delay | 0.05-0.5 | Echo amount |
|
||||
| Filter | filter | 500-8000 | Low-pass cutoff (Hz) |
|
||||
|
||||
### Song Structure
|
||||
|
||||
Each song defines:
|
||||
- **Sections**: intro, verse, chorus, bridge, break, outro
|
||||
- **Per Section**: bars, chord progression, melody style, bass style, drum pattern, intensity
|
||||
|
||||
### Scales Available
|
||||
|
||||
| Scale | Intervals | Typical Mood |
|
||||
|-------|-----------|--------------|
|
||||
| major | 0,2,4,5,7,9,11 | Happy, triumphant |
|
||||
| minor | 0,2,3,5,7,8,10 | Sad, dramatic |
|
||||
| dorian | 0,2,3,5,7,9,10 | Jazzy, mysterious |
|
||||
| phrygian | 0,1,3,5,7,8,10 | Dark, exotic |
|
||||
| lydian | 0,2,4,6,7,9,11 | Dreamy, bright |
|
||||
| mixolydian | 0,2,4,5,7,9,10 | Bluesy, groovy |
|
||||
| harmonicMinor | 0,2,3,5,7,8,11 | Dramatic, Eastern |
|
||||
| melodicMinor | 0,2,3,5,7,9,11 | Jazz, sophisticated |
|
||||
| pentatonicMajor | 0,2,4,7,9 | Simple, folk |
|
||||
| pentatonicMinor | 0,3,5,7,10 | Blues, rock |
|
||||
| blues | 0,3,5,6,7,10 | Blues, funky |
|
||||
| wholeTone | 0,2,4,6,8,10 | Dreamy, unsettling |
|
||||
| chromatic | all 12 | Atonal, chaotic |
|
||||
|
||||
---
|
||||
|
||||
## Statistics Summary
|
||||
|
||||
| Mood | Songs | Energy Range | Tempo Range | Top Tags |
|
||||
|------|-------|--------------|-------------|----------|
|
||||
| Epic | 20 | 7-10 | 120-170 | battle, heroic, powerful |
|
||||
| Chill | 20 | 1-4 | 70-110 | peaceful, calm, soft |
|
||||
| Intense | 20 | 8-10 | 142-180 | aggressive, fast, urgent |
|
||||
| Playful | 20 | 4-7 | 110-150 | fun, bouncy, happy |
|
||||
| Mysterious | 20 | 2-5 | 80-120 | dark, enigma, eerie |
|
||||
| Heroic | 20 | 6-9 | 120-160 | triumph, courage, glory |
|
||||
| Quirky | 20 | 5-8 | 100-140 | funny, wacky, eccentric |
|
||||
| Ambient | 20 | 1-3 | 60-85 | floating, tranquil, vast |
|
||||
|
||||
**Total: 160 songs**
|
||||
@@ -0,0 +1,546 @@
|
||||
# Preset Guide
|
||||
|
||||
## Overview
|
||||
|
||||
220 curated asset combinations providing instant variety for 11 game styles.
|
||||
Each preset includes coordinated music, background, and avatar context.
|
||||
|
||||
**Total Presets:** 220
|
||||
**Presets per Style:** 20
|
||||
**Game Styles:** 11
|
||||
|
||||
---
|
||||
|
||||
## Game Styles
|
||||
|
||||
### Action/Arcade (20 presets)
|
||||
|
||||
Fast-paced action games with intense gameplay and dynamic visuals.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| action-space-01 | Space Hero | Epic, 9 | space/nebula | spaceship-cockpit | Space shooters |
|
||||
| action-space-02 | Asteroid Runner | Intense, 8 | space/asteroidField | spaceship-cockpit | Dodging games |
|
||||
| action-platform-01 | Retro Runner | Playful, 7 | retro/pixelGrid | platformer-standard | Platformers |
|
||||
| action-platform-02 | Jungle Dash | Heroic, 8 | nature/jungle | adventure-hero | Jungle runners |
|
||||
| action-tank-01 | Tank Commander | Heroic, 9 | urban/citySkyline | tank | Military games |
|
||||
| action-tank-02 | Desert Storm | Epic, 8 | nature/desert | tank | Desert warfare |
|
||||
| action-mech-01 | Mech Warrior | Epic, 10 | mechanical/robotFactory | mech-suit | Robot battles |
|
||||
| action-mech-02 | Titan Fall | Intense, 9 | space/planets | mech-suit | Sci-fi combat |
|
||||
| action-jetpack-01 | Jetpack Joyride | Playful, 7 | sky/clouds | jetpack | Flying games |
|
||||
| action-jetpack-02 | Volcano Escape | Intense, 9 | nature/mountains | jetpack | Escape games |
|
||||
| action-ninja-01 | Shadow Strike | Mysterious, 7 | fantasy/temple | ninja-outfit | Stealth action |
|
||||
| action-ninja-02 | Rooftop Runner | Intense, 8 | urban/rooftop | ninja-outfit | Parkour games |
|
||||
| action-shooter-01 | Alien Blaster | Epic, 9 | space/deepSpace | spaceship-cockpit | Shoot-em-ups |
|
||||
| action-shooter-02 | Robot Rampage | Intense, 8 | mechanical/factory | superhero-cape | Wave shooters |
|
||||
| action-flying-01 | Sky Ace | Heroic, 8 | sky/flyingHigh | airplane | Dogfight games |
|
||||
| action-flying-02 | Storm Chaser | Intense, 9 | weather/storm | airplane | Weather dodging |
|
||||
| action-underwater-01 | Deep Sea Diver | Mysterious, 6 | underwater/deepSea | submarine | Ocean exploration |
|
||||
| action-underwater-02 | Submarine Command | Heroic, 7 | underwater/submarine | submarine | Naval combat |
|
||||
| action-superhero-01 | Cape Crusader | Heroic, 9 | urban/citySkyline | superhero-cape | Superhero games |
|
||||
| action-arcade-classic | Arcade Classic | Playful, 8 | retro/arcadeCabinet | platformer-standard | Retro arcade |
|
||||
|
||||
---
|
||||
|
||||
### Puzzle (20 presets)
|
||||
|
||||
Brain-teasing puzzle games with calm, focused atmospheres.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| puzzle-zen-01 | Zen Garden | Chill, 2 | nature/meadow | platformer-standard | Matching games |
|
||||
| puzzle-zen-02 | Bamboo Sanctuary | Ambient, 2 | nature/forest | platformer-standard | Tile puzzles |
|
||||
| puzzle-abstract-01 | Geometric Dreams | Ambient, 3 | abstract/geometricPatterns | platformer-standard | Pattern games |
|
||||
| puzzle-abstract-02 | Color Flow | Chill, 3 | abstract/gradients | platformer-standard | Color matching |
|
||||
| puzzle-lab-01 | Science Lab | Quirky, 4 | indoor/laboratory | scientist-labcoat | Chemistry puzzles |
|
||||
| puzzle-lab-02 | Brain Teasers | Quirky, 4 | indoor/library | scientist-labcoat | Logic puzzles |
|
||||
| puzzle-colorful-01 | Candy Crush | Playful, 5 | colorful/candyWorld | platformer-standard | Match-3 games |
|
||||
| puzzle-colorful-02 | Rainbow Bridge | Playful, 4 | colorful/rainbow | platformer-standard | Color puzzles |
|
||||
| puzzle-space-01 | Cosmic Puzzle | Ambient, 3 | space/nebula | space-suit | Space puzzles |
|
||||
| puzzle-space-02 | Starlight Logic | Mysterious, 3 | space/deepSpace | space-suit | Constellation puzzles |
|
||||
| puzzle-library-01 | Ancient Library | Mysterious, 4 | fantasy/magicalLibrary | wizard-robes | Word puzzles |
|
||||
| puzzle-library-02 | Scholar's Study | Chill, 3 | indoor/library | wizard-robes | Book puzzles |
|
||||
| puzzle-ocean-01 | Ocean Depths | Ambient, 2 | underwater/coralReef | platformer-standard | Marine puzzles |
|
||||
| puzzle-ocean-02 | Bubble Pop | Playful, 4 | underwater/kelpForest | platformer-standard | Bubble games |
|
||||
| puzzle-crystal-01 | Crystal Cave | Mysterious, 4 | fantasy/crystalCave | platformer-standard | Gem puzzles |
|
||||
| puzzle-crystal-02 | Jewel Quest | Playful, 5 | fantasy/crystalCave | adventure-hero | Gem matching |
|
||||
| puzzle-cloud-01 | Cloud Kingdom | Ambient, 2 | sky/clouds | platformer-standard | Sky puzzles |
|
||||
| puzzle-cloud-02 | Dream Weaver | Chill, 2 | sky/horizon | platformer-standard | Dream puzzles |
|
||||
| puzzle-minimalist-01 | Minimal Mind | Ambient, 2 | abstract/minimalist | platformer-standard | Minimal puzzles |
|
||||
| puzzle-garden-01 | Garden Puzzle | Chill, 3 | seasonal/springMeadow | platformer-standard | Nature puzzles |
|
||||
|
||||
---
|
||||
|
||||
### Story Adventure (20 presets)
|
||||
|
||||
Narrative-driven adventures with immersive world-building.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| story-fantasy-01 | Dragon Quest | Heroic, 8 | fantasy/castle | knight-armor | Fantasy RPGs |
|
||||
| story-fantasy-02 | Enchanted Forest | Mysterious, 6 | fantasy/enchantedForest | adventure-hero | Fairy tales |
|
||||
| story-wizard-01 | Wizard Academy | Quirky, 6 | fantasy/magicalLibrary | wizard-robes | Magic schools |
|
||||
| story-wizard-02 | Spell Caster | Epic, 7 | fantasy/tower | wizard-robes | Magic adventures |
|
||||
| story-pirate-01 | Pirate's Treasure | Heroic, 7 | nature/ocean | pirate-outfit | Pirate adventures |
|
||||
| story-pirate-02 | Shipwreck Island | Mysterious, 6 | underwater/shipwreck | pirate-outfit | Island mysteries |
|
||||
| story-nature-01 | Forest Journey | Chill, 5 | nature/forest | adventure-hero | Nature quests |
|
||||
| story-nature-02 | Mountain Expedition | Heroic, 6 | nature/mountains | adventure-hero | Climbing adventures |
|
||||
| story-urban-01 | City Detective | Mysterious, 5 | urban/street | platformer-standard | Mystery solving |
|
||||
| story-urban-02 | Night City | Mysterious, 6 | urban/neonDistrict | ninja-outfit | Noir adventures |
|
||||
| story-spooky-01 | Haunted Manor | Mysterious, 5 | spooky/hauntedHouse | platformer-standard | Ghost stories |
|
||||
| story-spooky-02 | Graveyard Shift | Mysterious, 6 | spooky/graveyard | ninja-outfit | Horror adventures |
|
||||
| story-medieval-01 | Knight's Honor | Heroic, 7 | fantasy/castle | knight-armor | Medieval quests |
|
||||
| story-medieval-02 | Village Hero | Playful, 6 | fantasy/castle | adventure-hero | Village adventures |
|
||||
| story-scifi-01 | Space Explorer | Ambient, 5 | space/spaceStation | space-suit | Sci-fi exploration |
|
||||
| story-scifi-02 | Alien Contact | Mysterious, 6 | space/planets | space-suit | First contact stories |
|
||||
| story-fairy-01 | Fairy Tale | Playful, 4 | fantasy/enchantedForest | wizard-robes | Classic fairy tales |
|
||||
| story-fairy-02 | Magic Kingdom | Heroic, 5 | fantasy/floatingIslands | adventure-hero | Fantasy kingdoms |
|
||||
| story-desert-01 | Desert Wanderer | Ambient, 5 | nature/desert | cowboy-gear | Desert adventures |
|
||||
| story-desert-02 | Oasis Quest | Heroic, 6 | nature/desert | adventure-hero | Desert exploration |
|
||||
|
||||
---
|
||||
|
||||
### Racing (20 presets)
|
||||
|
||||
High-speed racing games with adrenaline-pumping action.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| racing-street-01 | Street Racer | Intense, 9 | urban/street | race-car | Street racing |
|
||||
| racing-street-02 | Midnight Run | Intense, 8 | urban/neonDistrict | race-car | Night racing |
|
||||
| racing-track-01 | Grand Prix | Epic, 9 | sports/raceTrack | race-car | Track racing |
|
||||
| racing-track-02 | Speed Champion | Heroic, 9 | sports/stadium | race-car | Championship races |
|
||||
| racing-motorcycle-01 | Moto Mayhem | Intense, 9 | urban/street | motorcycle | Motorcycle racing |
|
||||
| racing-motorcycle-02 | Highway Rider | Heroic, 8 | nature/mountains | motorcycle | Highway racing |
|
||||
| racing-hover-01 | Hover Rush | Epic, 9 | abstract/waves | hoverboard | Futuristic racing |
|
||||
| racing-hover-02 | Neon Glide | Intense, 8 | retro/synthwave | hoverboard | Cyberpunk racing |
|
||||
| racing-runner-01 | Endless Runner | Playful, 7 | urban/street | runner | Endless runners |
|
||||
| racing-runner-02 | Temple Sprint | Heroic, 8 | fantasy/temple | runner | Temple runs |
|
||||
| racing-offroad-01 | Offroad Rally | Intense, 8 | nature/mountains | race-car | Rally racing |
|
||||
| racing-offroad-02 | Mud Madness | Heroic, 7 | nature/forest | race-car | Offroad racing |
|
||||
| racing-boat-01 | Wave Rider | Heroic, 8 | nature/ocean | platformer-standard | Boat racing |
|
||||
| racing-boat-02 | River Rush | Intense, 7 | nature/waterfall | platformer-standard | River racing |
|
||||
| racing-sky-01 | Sky Race | Epic, 9 | sky/flyingHigh | airplane | Air racing |
|
||||
| racing-sky-02 | Cloud Dash | Playful, 7 | sky/clouds | flappy-style | Sky dodging |
|
||||
| racing-kart-01 | Kart Kingdom | Playful, 7 | colorful/rainbow | race-car | Kart racing |
|
||||
| racing-kart-02 | Turbo Toons | Playful, 8 | colorful/candyWorld | race-car | Cartoon racing |
|
||||
| racing-winter-01 | Snow Drift | Heroic, 7 | seasonal/winterWonderland | race-car | Winter racing |
|
||||
| racing-winter-02 | Ice Racer | Intense, 8 | weather/snow | race-car | Ice track racing |
|
||||
|
||||
---
|
||||
|
||||
### Pet/Simulator (20 presets)
|
||||
|
||||
Relaxing pet care and life simulation games.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| pet-home-01 | Cozy Home | Chill, 3 | indoor/bedroom | platformer-standard | Virtual pets |
|
||||
| pet-home-02 | Pet Palace | Playful, 4 | indoor/bedroom | platformer-standard | Pet mansions |
|
||||
| pet-garden-01 | Garden Friends | Chill, 3 | nature/meadow | platformer-standard | Outdoor pets |
|
||||
| pet-garden-02 | Butterfly Garden | Ambient, 2 | seasonal/springMeadow | platformer-standard | Insect pets |
|
||||
| pet-farm-01 | Farm Life | Playful, 4 | nature/meadow | platformer-standard | Farm animals |
|
||||
| pet-farm-02 | Barn Buddies | Playful, 5 | nature/forest | platformer-standard | Farm simulation |
|
||||
| pet-aquarium-01 | Aqua World | Ambient, 2 | underwater/coralReef | platformer-standard | Fish tanks |
|
||||
| pet-aquarium-02 | Deep Sea Friends | Chill, 3 | underwater/kelpForest | platformer-standard | Ocean pets |
|
||||
| pet-park-01 | Pet Park | Playful, 4 | urban/park | platformer-standard | Dog walking |
|
||||
| pet-park-02 | Playground Pals | Playful, 5 | urban/park | platformer-standard | Pet playgrounds |
|
||||
| pet-cafe-01 | Pet Cafe | Chill, 3 | indoor/kitchen | chef-outfit | Cat cafes |
|
||||
| pet-cafe-02 | Treat Shop | Playful, 4 | colorful/candyWorld | chef-outfit | Pet treats |
|
||||
| pet-exotic-01 | Exotic Sanctuary | Mysterious, 4 | nature/jungle | adventure-hero | Exotic pets |
|
||||
| pet-exotic-02 | Rainforest Refuge | Ambient, 3 | nature/jungle | adventure-hero | Jungle animals |
|
||||
| pet-wildlife-01 | Wildlife Rescue | Heroic, 5 | nature/forest | adventure-hero | Animal rescue |
|
||||
| pet-wildlife-02 | Safari Friends | Playful, 4 | nature/desert | adventure-hero | Safari animals |
|
||||
| pet-virtual-01 | Cyber Pet | Quirky, 4 | retro/pixelGrid | platformer-standard | Digital pets |
|
||||
| pet-virtual-02 | Pixel Pals | Playful, 5 | retro/eightBit | platformer-standard | Retro virtual pets |
|
||||
| pet-clinic-01 | Vet Clinic | Chill, 4 | indoor/laboratory | scientist-labcoat | Vet games |
|
||||
| pet-grooming-01 | Grooming Salon | Playful, 3 | colorful/paintSplatter | platformer-standard | Pet grooming |
|
||||
|
||||
---
|
||||
|
||||
### Trivia/Quiz (20 presets)
|
||||
|
||||
Educational quiz games with engaging visual themes.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| trivia-gameshow-01 | Game Show Star | Playful, 6 | colorful/disco | platformer-standard | TV quiz shows |
|
||||
| trivia-gameshow-02 | Quiz Champion | Epic, 7 | colorful/neon | platformer-standard | Championship quizzes |
|
||||
| trivia-classroom-01 | Classroom Quiz | Quirky, 4 | indoor/classroom | scientist-labcoat | School quizzes |
|
||||
| trivia-classroom-02 | Study Session | Chill, 3 | indoor/library | wizard-robes | Study games |
|
||||
| trivia-space-01 | Space Academy | Mysterious, 5 | space/nebula | space-suit | Astronomy quizzes |
|
||||
| trivia-space-02 | Cosmic Quiz | Ambient, 4 | space/deepSpace | space-suit | Space trivia |
|
||||
| trivia-history-01 | Time Traveler | Heroic, 5 | fantasy/castle | knight-armor | History quizzes |
|
||||
| trivia-history-02 | Ancient Wisdom | Mysterious, 4 | fantasy/temple | wizard-robes | Ancient history |
|
||||
| trivia-nature-01 | Nature Explorer | Chill, 4 | nature/forest | adventure-hero | Nature quizzes |
|
||||
| trivia-nature-02 | Wildlife Quiz | Playful, 4 | nature/jungle | adventure-hero | Animal trivia |
|
||||
| trivia-pop-01 | Pop Culture | Playful, 6 | colorful/neon | platformer-standard | Pop culture quizzes |
|
||||
| trivia-pop-02 | Music Mania | Playful, 7 | colorful/disco | platformer-standard | Music trivia |
|
||||
| trivia-sports-01 | Sports Fanatic | Heroic, 6 | sports/stadium | sports-player | Sports quizzes |
|
||||
| trivia-sports-02 | Champion Trivia | Epic, 7 | sports/arena | sports-player | Sports history |
|
||||
| trivia-geography-01 | World Explorer | Heroic, 5 | sky/flyingHigh | adventure-hero | Geography quizzes |
|
||||
| trivia-geography-02 | Map Master | Chill, 4 | sky/horizon | adventure-hero | Map quizzes |
|
||||
| trivia-food-01 | Foodie Quiz | Playful, 5 | indoor/kitchen | chef-outfit | Food trivia |
|
||||
| trivia-food-02 | Chef Challenge | Quirky, 6 | colorful/candyWorld | chef-outfit | Cooking quizzes |
|
||||
| trivia-math-01 | Math Genius | Quirky, 5 | abstract/geometricPatterns | scientist-labcoat | Math quizzes |
|
||||
| trivia-general-01 | Know It All | Playful, 5 | colorful/rainbow | platformer-standard | General knowledge |
|
||||
|
||||
---
|
||||
|
||||
### Music/Rhythm (20 presets)
|
||||
|
||||
Beat-matching and rhythm games with vibrant audio-visual experiences.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| rhythm-disco-01 | Disco Fever | Playful, 8 | colorful/disco | platformer-standard | Disco rhythm |
|
||||
| rhythm-disco-02 | Dance Floor | Intense, 8 | colorful/neon | platformer-standard | Dancing games |
|
||||
| rhythm-retro-01 | Arcade Beats | Playful, 7 | retro/arcadeCabinet | platformer-standard | Retro rhythm |
|
||||
| rhythm-retro-02 | Pixel Grooves | Quirky, 6 | retro/pixelGrid | platformer-standard | 8-bit music |
|
||||
| rhythm-concert-01 | Rock Concert | Epic, 9 | colorful/neon | superhero-cape | Rock rhythm |
|
||||
| rhythm-concert-02 | Stage Star | Heroic, 8 | colorful/disco | platformer-standard | Concert games |
|
||||
| rhythm-space-01 | Cosmic Beats | Ambient, 6 | space/nebula | space-suit | Space rhythm |
|
||||
| rhythm-space-02 | Stellar Symphony | Mysterious, 5 | space/deepSpace | space-suit | Ambient music |
|
||||
| rhythm-street-01 | Street Beat | Intense, 8 | urban/street | ninja-outfit | Hip-hop rhythm |
|
||||
| rhythm-street-02 | Urban Flow | Heroic, 7 | urban/neonDistrict | platformer-standard | Street dancing |
|
||||
| rhythm-tropical-01 | Island Vibes | Playful, 6 | seasonal/tropical | platformer-standard | Tropical rhythm |
|
||||
| rhythm-tropical-02 | Beach Party | Playful, 7 | seasonal/summerBeach | platformer-standard | Beach music |
|
||||
| rhythm-abstract-01 | Visual Beats | Ambient, 5 | abstract/waves | platformer-standard | Abstract rhythm |
|
||||
| rhythm-abstract-02 | Synth Wave | Quirky, 6 | abstract/kaleidoscope | platformer-standard | Synth music |
|
||||
| rhythm-classical-01 | Symphony Hall | Chill, 4 | indoor/library | wizard-robes | Classical music |
|
||||
| rhythm-classical-02 | Piano Master | Ambient, 3 | indoor/arcade | platformer-standard | Piano rhythm |
|
||||
| rhythm-festival-01 | Festival Fever | Epic, 9 | colorful/tieDye | platformer-standard | Festival music |
|
||||
| rhythm-festival-02 | Rave Night | Intense, 10 | colorful/neon | ninja-outfit | Rave games |
|
||||
| rhythm-jazz-01 | Jazz Lounge | Chill, 4 | urban/alley | platformer-standard | Jazz rhythm |
|
||||
| rhythm-drums-01 | Drum Circle | Heroic, 7 | nature/jungle | adventure-hero | Drum games |
|
||||
|
||||
---
|
||||
|
||||
### Creative/Drawing (20 presets)
|
||||
|
||||
Art and creativity games encouraging self-expression.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| creative-studio-01 | Art Studio | Chill, 3 | indoor/classroom | platformer-standard | Drawing games |
|
||||
| creative-studio-02 | Paint Paradise | Playful, 4 | colorful/paintSplatter | platformer-standard | Painting games |
|
||||
| creative-nature-01 | Nature Sketch | Ambient, 3 | nature/meadow | adventure-hero | Nature drawing |
|
||||
| creative-nature-02 | Forest Canvas | Chill, 2 | nature/forest | adventure-hero | Landscape art |
|
||||
| creative-colorful-01 | Color Splash | Playful, 5 | colorful/rainbow | platformer-standard | Coloring games |
|
||||
| creative-colorful-02 | Neon Art | Quirky, 6 | colorful/neon | platformer-standard | Glow art |
|
||||
| creative-abstract-01 | Abstract Express | Ambient, 4 | abstract/gradients | platformer-standard | Abstract art |
|
||||
| creative-abstract-02 | Shape Maker | Chill, 3 | abstract/geometricPatterns | platformer-standard | Geometric art |
|
||||
| creative-kids-01 | Crayon World | Playful, 5 | colorful/candyWorld | platformer-standard | Kids drawing |
|
||||
| creative-kids-02 | Doodle Town | Playful, 6 | colorful/paintSplatter | platformer-standard | Doodling games |
|
||||
| creative-fantasy-01 | Dragon Painter | Heroic, 5 | fantasy/enchantedForest | wizard-robes | Fantasy art |
|
||||
| creative-fantasy-02 | Magic Canvas | Mysterious, 4 | fantasy/magicalLibrary | wizard-robes | Magical drawing |
|
||||
| creative-space-01 | Galaxy Art | Ambient, 3 | space/nebula | space-suit | Space art |
|
||||
| creative-space-02 | Cosmic Creator | Mysterious, 4 | space/deepSpace | space-suit | Universe creation |
|
||||
| creative-beach-01 | Beach Doodles | Chill, 3 | seasonal/summerBeach | platformer-standard | Beach art |
|
||||
| creative-beach-02 | Sand Art | Ambient, 2 | nature/desert | platformer-standard | Sand drawing |
|
||||
| creative-city-01 | City Sketch | Quirky, 4 | urban/citySkyline | platformer-standard | Urban art |
|
||||
| creative-city-02 | Street Art | Playful, 5 | urban/alley | ninja-outfit | Graffiti games |
|
||||
| creative-pixel-01 | Pixel Art | Quirky, 5 | retro/pixelGrid | platformer-standard | Pixel drawing |
|
||||
| creative-food-01 | Food Art | Playful, 4 | indoor/kitchen | chef-outfit | Food decoration |
|
||||
|
||||
---
|
||||
|
||||
### RPG/Battle (20 presets)
|
||||
|
||||
Role-playing and combat games with epic adventures.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| rpg-fantasy-01 | Kingdom Quest | Epic, 8 | fantasy/castle | knight-armor | Fantasy RPGs |
|
||||
| rpg-fantasy-02 | Hero's Journey | Heroic, 7 | fantasy/enchantedForest | adventure-hero | Adventure RPGs |
|
||||
| rpg-wizard-01 | Arcane Academy | Mysterious, 6 | fantasy/magicalLibrary | wizard-robes | Magic RPGs |
|
||||
| rpg-wizard-02 | Sorcerer Supreme | Epic, 8 | fantasy/tower | wizard-robes | Wizard battles |
|
||||
| rpg-ninja-01 | Ninja Chronicles | Intense, 7 | fantasy/temple | ninja-outfit | Ninja RPGs |
|
||||
| rpg-ninja-02 | Shadow Warrior | Mysterious, 8 | spooky/darkForest | ninja-outfit | Stealth RPGs |
|
||||
| rpg-dragon-01 | Dragon Rider | Epic, 9 | sky/flyingHigh | dragon-rider | Dragon games |
|
||||
| rpg-dragon-02 | Dragon Slayer | Heroic, 9 | fantasy/floatingIslands | knight-armor | Dragon battles |
|
||||
| rpg-dungeon-01 | Dungeon Crawler | Mysterious, 7 | fantasy/dungeon | knight-armor | Dungeon games |
|
||||
| rpg-dungeon-02 | Crypt Explorer | Mysterious, 6 | spooky/crypt | adventure-hero | Crypt exploration |
|
||||
| rpg-spooky-01 | Ghost Hunter | Mysterious, 6 | spooky/hauntedHouse | adventure-hero | Ghost hunting |
|
||||
| rpg-spooky-02 | Vampire Slayer | Intense, 8 | spooky/manor | knight-armor | Monster hunting |
|
||||
| rpg-nature-01 | Forest Guardian | Heroic, 6 | nature/forest | adventure-hero | Nature RPGs |
|
||||
| rpg-nature-02 | Wilderness Hero | Playful, 5 | nature/meadow | adventure-hero | Exploration RPGs |
|
||||
| rpg-arena-01 | Battle Arena | Epic, 9 | sports/arena | knight-armor | Arena battles |
|
||||
| rpg-arena-02 | Gladiator Glory | Intense, 9 | sports/stadium | superhero-cape | Gladiator games |
|
||||
| rpg-ice-01 | Frost Warrior | Heroic, 7 | seasonal/winterWonderland | knight-armor | Ice RPGs |
|
||||
| rpg-ice-02 | Blizzard Quest | Intense, 8 | weather/snow | adventure-hero | Winter quests |
|
||||
| rpg-volcano-01 | Lava Lord | Epic, 9 | nature/mountains | knight-armor | Volcano adventures |
|
||||
| rpg-boss-01 | Boss Battle | Epic, 10 | fantasy/dungeon | superhero-cape | Boss fights |
|
||||
|
||||
---
|
||||
|
||||
### Sports (20 presets)
|
||||
|
||||
Athletic competition games across various sports.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| sports-soccer-01 | Soccer Star | Heroic, 8 | sports/field | sports-player | Soccer games |
|
||||
| sports-soccer-02 | World Cup | Epic, 9 | sports/stadium | sports-player | Soccer tournaments |
|
||||
| sports-basketball-01 | Court Kings | Heroic, 8 | sports/court | sports-player | Basketball games |
|
||||
| sports-basketball-02 | Slam Dunk | Epic, 9 | sports/arena | sports-player | Basketball action |
|
||||
| sports-baseball-01 | Home Run Hero | Heroic, 7 | sports/field | sports-player | Baseball games |
|
||||
| sports-baseball-02 | World Series | Epic, 8 | sports/stadium | sports-player | Baseball tournaments |
|
||||
| sports-tennis-01 | Tennis Ace | Playful, 6 | sports/court | sports-player | Tennis games |
|
||||
| sports-tennis-02 | Grand Slam | Heroic, 7 | sports/stadium | sports-player | Tennis tournaments |
|
||||
| sports-golf-01 | Golf Master | Chill, 4 | nature/meadow | sports-player | Golf games |
|
||||
| sports-golf-02 | Championship Green | Ambient, 5 | sports/field | sports-player | Golf tournaments |
|
||||
| sports-swimming-01 | Pool Champion | Heroic, 6 | sports/pool | sports-player | Swimming races |
|
||||
| sports-swimming-02 | Dive Star | Playful, 5 | underwater/coralReef | sports-player | Diving games |
|
||||
| sports-football-01 | Touchdown Hero | Epic, 8 | sports/field | sports-player | Football games |
|
||||
| sports-football-02 | Super Bowl | Epic, 9 | sports/stadium | sports-player | Football tournaments |
|
||||
| sports-hockey-01 | Ice Champion | Intense, 8 | sports/arena | sports-player | Hockey games |
|
||||
| sports-hockey-02 | Puck Master | Heroic, 7 | seasonal/winterWonderland | sports-player | Ice hockey |
|
||||
| sports-boxing-01 | Ring Champion | Intense, 9 | sports/arena | superhero-cape | Boxing games |
|
||||
| sports-boxing-02 | Knockout King | Epic, 9 | sports/gym | superhero-cape | Fighting games |
|
||||
| sports-extreme-01 | Extreme Games | Intense, 8 | nature/mountains | adventure-hero | Extreme sports |
|
||||
| sports-extreme-02 | X-Games Hero | Heroic, 9 | sports/skatepark | adventure-hero | Skateboarding |
|
||||
|
||||
---
|
||||
|
||||
### Physics/Pinball (20 presets)
|
||||
|
||||
Physics-based puzzle games and pinball simulations.
|
||||
|
||||
| ID | Name | Music | Background | Context | Best For |
|
||||
|----|------|-------|------------|---------|----------|
|
||||
| physics-pinball-01 | Classic Pinball | Playful, 7 | retro/arcadeCabinet | platformer-standard | Classic pinball |
|
||||
| physics-pinball-02 | Neon Pinball | Intense, 8 | colorful/neon | platformer-standard | Modern pinball |
|
||||
| physics-retro-01 | Retro Physics | Quirky, 6 | retro/pixelGrid | platformer-standard | Retro physics |
|
||||
| physics-retro-02 | Arcade Physics | Playful, 7 | retro/arcadeCabinet | platformer-standard | Arcade physics |
|
||||
| physics-mechanical-01 | Gear Works | Quirky, 5 | mechanical/gears | scientist-labcoat | Gear puzzles |
|
||||
| physics-mechanical-02 | Factory Fun | Quirky, 6 | mechanical/factory | scientist-labcoat | Factory games |
|
||||
| physics-colorful-01 | Bouncy World | Playful, 6 | colorful/rainbow | platformer-standard | Bouncing games |
|
||||
| physics-colorful-02 | Candy Physics | Playful, 5 | colorful/candyWorld | platformer-standard | Candy physics |
|
||||
| physics-gravity-01 | Zero Gravity | Ambient, 4 | space/deepSpace | space-suit | Gravity puzzles |
|
||||
| physics-gravity-02 | Orbital Mechanics | Mysterious, 5 | space/planets | space-suit | Space physics |
|
||||
| physics-destruction-01 | Demolition Derby | Intense, 8 | urban/citySkyline | superhero-cape | Destruction games |
|
||||
| physics-destruction-02 | Crash Test | Heroic, 7 | mechanical/factory | platformer-standard | Crash physics |
|
||||
| physics-water-01 | Water Works | Chill, 4 | underwater/coralReef | platformer-standard | Water physics |
|
||||
| physics-water-02 | Fluid Dynamics | Ambient, 3 | abstract/waves | scientist-labcoat | Fluid games |
|
||||
| physics-balance-01 | Stack Master | Quirky, 5 | abstract/minimalist | platformer-standard | Stacking games |
|
||||
| physics-balance-02 | Tower Builder | Playful, 6 | urban/citySkyline | platformer-standard | Building games |
|
||||
| physics-launch-01 | Launcher Pro | Heroic, 7 | sky/clouds | platformer-standard | Launch games |
|
||||
| physics-launch-02 | Catapult King | Epic, 8 | fantasy/castle | knight-armor | Siege games |
|
||||
| physics-pendulum-01 | Pendulum Puzzle | Chill, 4 | abstract/geometricPatterns | scientist-labcoat | Pendulum games |
|
||||
| physics-rube-01 | Rube Goldberg | Quirky, 6 | mechanical/conveyorBelts | scientist-labcoat | Chain reaction |
|
||||
|
||||
---
|
||||
|
||||
## Using Presets
|
||||
|
||||
### Get Preset by ID
|
||||
|
||||
```javascript
|
||||
const preset = AssetPresets.getById('action-space-01');
|
||||
// Returns: { id, name, music, background, avatarContext, ... }
|
||||
```
|
||||
|
||||
### Get All for Game Style
|
||||
|
||||
```javascript
|
||||
const actionPresets = AssetPresets.getByGameStyle('action-arcade');
|
||||
// Returns array of 20 presets
|
||||
```
|
||||
|
||||
### Get Random Preset
|
||||
|
||||
```javascript
|
||||
const random = AssetPresets.getRandom('racing');
|
||||
// Returns single random preset from racing category
|
||||
```
|
||||
|
||||
### Search Presets
|
||||
|
||||
```javascript
|
||||
const matches = AssetPresets.search('dragon');
|
||||
// Returns all presets with "dragon" in name, description, or tags
|
||||
```
|
||||
|
||||
### Load Preset
|
||||
|
||||
```javascript
|
||||
const preset = AssetPresets.getById('action-space-01');
|
||||
const config = {
|
||||
music: preset.music,
|
||||
background: preset.background,
|
||||
avatarContext: preset.avatarContext
|
||||
};
|
||||
await AssetManager.loadGameAssets(config);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Customizing Presets
|
||||
|
||||
### Override Music
|
||||
|
||||
Keep the visual theme but change the music mood:
|
||||
|
||||
```javascript
|
||||
const preset = AssetPresets.getById('action-space-01');
|
||||
const custom = {
|
||||
...preset,
|
||||
music: { mood: 'Mysterious', energy: 5 } // Different mood
|
||||
};
|
||||
```
|
||||
|
||||
### Keep Music, Change Background
|
||||
|
||||
Swap the background while maintaining audio coherence:
|
||||
|
||||
```javascript
|
||||
const preset = AssetPresets.getById('puzzle-zen-01');
|
||||
const custom = {
|
||||
music: preset.music,
|
||||
background: { theme: 'underwater', variant: 'coralReef' },
|
||||
avatarContext: preset.avatarContext
|
||||
};
|
||||
```
|
||||
|
||||
### Change Avatar Context Only
|
||||
|
||||
Different character presentation with same environment:
|
||||
|
||||
```javascript
|
||||
const preset = AssetPresets.getById('racing-street-01');
|
||||
const custom = {
|
||||
...preset,
|
||||
avatarContext: { mode: 'FULL_BODY', context: 'motorcycle' }
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Preset Variety Strategy
|
||||
|
||||
For maximum variety in generated games:
|
||||
|
||||
### 1. Track Recently Used Presets
|
||||
|
||||
```javascript
|
||||
const usedPresets = new Set();
|
||||
|
||||
function getUnusedPreset(gameStyle) {
|
||||
const presets = AssetPresets.getByGameStyle(gameStyle);
|
||||
const unused = presets.filter(p => !usedPresets.has(p.id));
|
||||
|
||||
if (unused.length === 0) {
|
||||
usedPresets.clear(); // Reset when all used
|
||||
return presets[0];
|
||||
}
|
||||
|
||||
const selected = unused[Math.floor(Math.random() * unused.length)];
|
||||
usedPresets.add(selected.id);
|
||||
return selected;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Cycle Through All 20 Presets
|
||||
|
||||
```javascript
|
||||
const presetIndex = {};
|
||||
|
||||
function getNextPreset(gameStyle) {
|
||||
if (!presetIndex[gameStyle]) presetIndex[gameStyle] = 0;
|
||||
|
||||
const presets = AssetPresets.getByGameStyle(gameStyle);
|
||||
const preset = presets[presetIndex[gameStyle]];
|
||||
|
||||
presetIndex[gameStyle] = (presetIndex[gameStyle] + 1) % presets.length;
|
||||
return preset;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Variation Functions
|
||||
|
||||
```javascript
|
||||
// Get variation of a preset
|
||||
const variation = AssetManager.getVariation(preset, 'background');
|
||||
// Same music and context, different background variant
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
All presets are pre-validated for compatibility. To check custom modifications:
|
||||
|
||||
```javascript
|
||||
const result = AssetCompatibility.validate(customConfig);
|
||||
|
||||
if (result.score < 70) {
|
||||
console.warn('Low compatibility score:', result.score);
|
||||
console.warn('Issues:', result.issues);
|
||||
}
|
||||
|
||||
// result.score: 0-100 compatibility score
|
||||
// result.issues: array of compatibility warnings
|
||||
```
|
||||
|
||||
### Compatibility Criteria
|
||||
|
||||
| Score Range | Quality |
|
||||
|-------------|---------|
|
||||
| 90-100 | Excellent - perfect harmony |
|
||||
| 70-89 | Good - works well |
|
||||
| 50-69 | Fair - noticeable mismatch |
|
||||
| 0-49 | Poor - significant issues |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Music Moods
|
||||
|
||||
| Mood | Energy Range | Best For |
|
||||
|------|--------------|----------|
|
||||
| Epic | 7-10 | Boss fights, climactic moments |
|
||||
| Heroic | 6-9 | Adventures, triumphs |
|
||||
| Intense | 8-10 | Chases, action sequences |
|
||||
| Playful | 4-7 | Fun, lighthearted games |
|
||||
| Mysterious | 2-5 | Puzzles, suspense |
|
||||
| Ambient | 1-3 | Calm, atmospheric |
|
||||
| Chill | 1-4 | Relaxation, menus |
|
||||
| Quirky | 4-8 | Comedy, eccentric |
|
||||
|
||||
### Background Themes
|
||||
|
||||
15 themes with 8 variants each (120 total):
|
||||
|
||||
- **space**: nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole
|
||||
- **nature**: forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow
|
||||
- **urban**: citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley
|
||||
- **fantasy**: castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower
|
||||
- **abstract**: geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix
|
||||
- **retro**: pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave
|
||||
- **indoor**: laboratory, classroom, bedroom, kitchen, arcade, office, library, gym
|
||||
- **weather**: rain, snow, storm, sunset, sunrise, fog, lightning, aurora
|
||||
- **sports**: stadium, raceTrack, court, field, arena, pool, gym, skatepark
|
||||
- **seasonal**: springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical
|
||||
- **underwater**: coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure
|
||||
- **sky**: clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield
|
||||
- **mechanical**: gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab
|
||||
- **spooky**: hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows
|
||||
- **colorful**: rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism
|
||||
|
||||
### Avatar Contexts
|
||||
|
||||
| Category | Contexts |
|
||||
|----------|----------|
|
||||
| **Vehicles** | spaceship-cockpit, race-car, airplane, flappy-style, tank, pacman-style, hoverboard, jetpack, mech-suit, submarine, dragon-rider, motorcycle |
|
||||
| **Costumes** | knight-armor, space-suit, ninja-outfit, wizard-robes, pirate-outfit, superhero-cape, adventure-hero, scientist-labcoat, chef-outfit, sports-player, cowboy-gear |
|
||||
| **Pure** | platformer-standard, runner |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Asset Library Overview](./ASSET_LIBRARY_OVERVIEW.md) - Complete asset system documentation
|
||||
- [Adding New Assets](./ADDING_NEW_ASSETS.md) - How to create new assets
|
||||
- [Game Generation Guide](./GAME_GENERATION_GUIDE.md) - How assets are used in generation
|
||||
@@ -0,0 +1,675 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Asset Comparison Demo - Same Game, Different Assets</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f0f1a;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #1a1a2e;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.game-panel {
|
||||
background: #1a1a2e;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 1.125rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #6366f1;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge.space { background: #3b82f6; }
|
||||
.badge.nature { background: #22c55e; }
|
||||
.badge.spooky { background: #8b5cf6; }
|
||||
.badge.retro { background: #f59e0b; }
|
||||
|
||||
.game-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/10;
|
||||
background: #0f0f1a;
|
||||
}
|
||||
|
||||
.game-frame canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #334155;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #252542;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #1a1a2e;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.controls h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #818cf8;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.btn.secondary:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.code-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.code-section h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.code-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.code-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #252542;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-tab.active {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #0f0f1a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.75rem;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
opacity: 0.9;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<h1>Asset Comparison Demo</h1>
|
||||
<p>The same dodge-the-obstacles game rendered with 4 different asset combinations</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="grid">
|
||||
<!-- Space Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Space Dodger</span></h2>
|
||||
<span class="badge space">Space Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game1" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">space/nebula</span>
|
||||
<span class="tag">Epic music</span>
|
||||
<span class="tag">spaceship-cockpit</span>
|
||||
<span class="tag">HEAD_ONLY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nature Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Forest Runner</span></h2>
|
||||
<span class="badge nature">Nature Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game2" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">nature/forest</span>
|
||||
<span class="tag">Playful music</span>
|
||||
<span class="tag">platformer-standard</span>
|
||||
<span class="tag">FULL_BODY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spooky Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Haunted Escape</span></h2>
|
||||
<span class="badge spooky">Spooky Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game3" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">spooky/hauntedHouse</span>
|
||||
<span class="tag">Mysterious music</span>
|
||||
<span class="tag">adventure-hero</span>
|
||||
<span class="tag">FULL_BODY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retro Theme -->
|
||||
<div class="game-panel">
|
||||
<div class="panel-header">
|
||||
<h2><span>Pixel Dash</span></h2>
|
||||
<span class="badge retro">Retro Theme</span>
|
||||
</div>
|
||||
<div class="game-frame">
|
||||
<canvas id="game4" width="640" height="400"></canvas>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="tag">retro/synthwave</span>
|
||||
<span class="tag">Quirky music</span>
|
||||
<span class="tag">runner</span>
|
||||
<span class="tag">FULL_BODY</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<h3>Global Controls</h3>
|
||||
<div class="control-row">
|
||||
<button class="btn" onclick="startAll()">Start All Games</button>
|
||||
<button class="btn secondary" onclick="pauseAll()">Pause All</button>
|
||||
<button class="btn secondary" onclick="resetAll()">Reset All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>How to Play</h3>
|
||||
<p>
|
||||
Use <strong>Arrow Keys</strong> or <strong>WASD</strong> to move. Avoid the obstacles!
|
||||
Each panel shows the same game logic with different visual assets.
|
||||
Notice how the background, music mood, avatar context, and rendering mode
|
||||
completely change the feel of the game while the core mechanics remain identical.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="code-section">
|
||||
<h3>Configuration Code</h3>
|
||||
<div class="code-tabs">
|
||||
<button class="code-tab active" onclick="showCode('space')">Space</button>
|
||||
<button class="code-tab" onclick="showCode('nature')">Nature</button>
|
||||
<button class="code-tab" onclick="showCode('spooky')">Spooky</button>
|
||||
<button class="code-tab" onclick="showCode('retro')">Retro</button>
|
||||
</div>
|
||||
<div class="code-block" id="codeDisplay">const GAME_CONFIG = {
|
||||
title: "Space Dodger",
|
||||
assets: {
|
||||
music: { mood: "Epic", energy: 8, enabled: true },
|
||||
background: { theme: "space", variant: "nebula", animated: true },
|
||||
avatarContext: {
|
||||
mode: "HEAD_ONLY",
|
||||
context: "spaceship-cockpit",
|
||||
showAccessories: true
|
||||
}
|
||||
}
|
||||
};</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Libraries -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/space.js"></script>
|
||||
<script src="/assets/backgrounds/themes/nature.js"></script>
|
||||
<script src="/assets/backgrounds/themes/spooky.js"></script>
|
||||
<script src="/assets/backgrounds/themes/retro.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
||||
<script src="/assets/avatar/contexts/pure.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// COMPARISON DEMO - Same game, 4 different asset sets
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const CONFIGS = {
|
||||
space: {
|
||||
title: "Space Dodger",
|
||||
assets: {
|
||||
music: { mood: "Epic", energy: 8 },
|
||||
background: { theme: "space", variant: "nebula" },
|
||||
avatar: { mode: "HEAD_ONLY", context: "spaceship-cockpit" }
|
||||
},
|
||||
colors: {
|
||||
player: '#3b82f6',
|
||||
obstacle: '#ef4444',
|
||||
coin: '#fbbf24'
|
||||
}
|
||||
},
|
||||
nature: {
|
||||
title: "Forest Runner",
|
||||
assets: {
|
||||
music: { mood: "Playful", energy: 6 },
|
||||
background: { theme: "nature", variant: "forest" },
|
||||
avatar: { mode: "FULL_BODY", context: "platformer-standard" }
|
||||
},
|
||||
colors: {
|
||||
player: '#22c55e',
|
||||
obstacle: '#a16207',
|
||||
coin: '#fbbf24'
|
||||
}
|
||||
},
|
||||
spooky: {
|
||||
title: "Haunted Escape",
|
||||
assets: {
|
||||
music: { mood: "Mysterious", energy: 5 },
|
||||
background: { theme: "spooky", variant: "hauntedHouse" },
|
||||
avatar: { mode: "FULL_BODY", context: "adventure-hero" }
|
||||
},
|
||||
colors: {
|
||||
player: '#a855f7',
|
||||
obstacle: '#7c3aed',
|
||||
coin: '#c084fc'
|
||||
}
|
||||
},
|
||||
retro: {
|
||||
title: "Pixel Dash",
|
||||
assets: {
|
||||
music: { mood: "Quirky", energy: 7 },
|
||||
background: { theme: "retro", variant: "synthwave" },
|
||||
avatar: { mode: "FULL_BODY", context: "runner" }
|
||||
},
|
||||
colors: {
|
||||
player: '#f59e0b',
|
||||
obstacle: '#ec4899',
|
||||
coin: '#06b6d4'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const playerAvatar = {
|
||||
faceShape: 'round',
|
||||
skinTone: 'medium',
|
||||
hairStyle: 'short',
|
||||
hairColor: '#3d2314',
|
||||
eyeShape: 'round',
|
||||
eyeColor: '#4a7c59'
|
||||
};
|
||||
|
||||
// Game instances
|
||||
const games = {};
|
||||
const keys = {};
|
||||
|
||||
// Input handling
|
||||
document.addEventListener('keydown', e => { keys[e.code] = true; });
|
||||
document.addEventListener('keyup', e => { keys[e.code] = false; });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME CLASS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
class DodgeGame {
|
||||
constructor(canvasId, config) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.config = config;
|
||||
|
||||
this.reset();
|
||||
this.running = false;
|
||||
this.lastTime = 0;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.playerX = this.canvas.width / 2;
|
||||
this.playerY = this.canvas.height - 80;
|
||||
this.score = 0;
|
||||
this.obstacles = [];
|
||||
this.coins = [];
|
||||
this.animTime = 0;
|
||||
this.spawnTimer = 0;
|
||||
this.coinTimer = 0;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.running = true;
|
||||
this.lastTime = performance.now();
|
||||
this.loop();
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
loop() {
|
||||
if (!this.running) return;
|
||||
|
||||
const now = performance.now();
|
||||
const dt = Math.min((now - this.lastTime) / 1000, 0.05);
|
||||
this.lastTime = now;
|
||||
|
||||
this.update(dt);
|
||||
this.render();
|
||||
|
||||
requestAnimationFrame(() => this.loop());
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
const speed = 200;
|
||||
|
||||
// Player movement
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) {
|
||||
this.playerX = Math.max(30, this.playerX - speed * dt);
|
||||
}
|
||||
if (keys['ArrowRight'] || keys['KeyD']) {
|
||||
this.playerX = Math.min(this.canvas.width - 30, this.playerX + speed * dt);
|
||||
}
|
||||
|
||||
// Spawn obstacles
|
||||
this.spawnTimer += dt;
|
||||
if (this.spawnTimer > 1.5) {
|
||||
this.spawnTimer = 0;
|
||||
this.obstacles.push({
|
||||
x: 50 + Math.random() * (this.canvas.width - 100),
|
||||
y: -30,
|
||||
size: 20 + Math.random() * 20
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn coins
|
||||
this.coinTimer += dt;
|
||||
if (this.coinTimer > 2) {
|
||||
this.coinTimer = 0;
|
||||
this.coins.push({
|
||||
x: 50 + Math.random() * (this.canvas.width - 100),
|
||||
y: -20
|
||||
});
|
||||
}
|
||||
|
||||
// Update obstacles
|
||||
this.obstacles = this.obstacles.filter(o => {
|
||||
o.y += 150 * dt;
|
||||
|
||||
// Collision
|
||||
const dx = this.playerX - o.x;
|
||||
const dy = this.playerY - o.y;
|
||||
if (Math.sqrt(dx*dx + dy*dy) < o.size + 25) {
|
||||
this.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
return o.y < this.canvas.height + 50;
|
||||
});
|
||||
|
||||
// Update coins
|
||||
this.coins = this.coins.filter(c => {
|
||||
c.y += 120 * dt;
|
||||
|
||||
// Collection
|
||||
const dx = this.playerX - c.x;
|
||||
const dy = this.playerY - c.y;
|
||||
if (Math.sqrt(dx*dx + dy*dy) < 35) {
|
||||
this.score += 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
return c.y < this.canvas.height + 30;
|
||||
});
|
||||
|
||||
this.score += Math.floor(dt * 5);
|
||||
this.animTime += dt * 1000;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ctx, canvas, config } = this;
|
||||
|
||||
// Background
|
||||
if (window.BackgroundEngine) {
|
||||
window.BackgroundEngine.render(
|
||||
ctx,
|
||||
config.assets.background.theme,
|
||||
config.assets.background.variant,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
this.animTime
|
||||
);
|
||||
} else {
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// Obstacles
|
||||
ctx.fillStyle = config.colors.obstacle;
|
||||
this.obstacles.forEach(o => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(o.x, o.y - o.size);
|
||||
ctx.lineTo(o.x + o.size, o.y + o.size);
|
||||
ctx.lineTo(o.x - o.size, o.y + o.size);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Coins
|
||||
ctx.fillStyle = config.colors.coin;
|
||||
this.coins.forEach(c => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(c.x, c.y, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Player
|
||||
ctx.save();
|
||||
ctx.translate(this.playerX, this.playerY);
|
||||
|
||||
const mode = config.assets.avatar.mode;
|
||||
const dims = {
|
||||
'HEAD_ONLY': { w: 64, h: 64, offsetY: 32 },
|
||||
'HEAD_AND_SHOULDERS': { w: 64, h: 96, offsetY: 48 },
|
||||
'UPPER_BODY': { w: 64, h: 128, offsetY: 64 },
|
||||
'FULL_BODY': { w: 64, h: 160, offsetY: 80 }
|
||||
};
|
||||
const d = dims[mode];
|
||||
|
||||
if (window.AvatarSystem) {
|
||||
ctx.translate(-32, -d.offsetY);
|
||||
window.AvatarSystem.render(
|
||||
ctx,
|
||||
playerAvatar,
|
||||
mode,
|
||||
'idle',
|
||||
this.animTime
|
||||
);
|
||||
} else {
|
||||
// Fallback
|
||||
ctx.fillStyle = config.colors.player;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 25, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Score
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 16px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`Score: ${this.score}`, 10, 25);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// CONTROLS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function startAll() {
|
||||
Object.values(games).forEach(g => g.start());
|
||||
}
|
||||
|
||||
function pauseAll() {
|
||||
Object.values(games).forEach(g => g.pause());
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
Object.values(games).forEach(g => {
|
||||
g.reset();
|
||||
g.render();
|
||||
});
|
||||
}
|
||||
|
||||
function showCode(theme) {
|
||||
document.querySelectorAll('.code-tab').forEach(t => t.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
const config = CONFIGS[theme];
|
||||
const code = `const GAME_CONFIG = {
|
||||
title: "${config.title}",
|
||||
assets: {
|
||||
music: { mood: "${config.assets.music.mood}", energy: ${config.assets.music.energy}, enabled: true },
|
||||
background: { theme: "${config.assets.background.theme}", variant: "${config.assets.background.variant}", animated: true },
|
||||
avatarContext: {
|
||||
mode: "${config.assets.avatar.mode}",
|
||||
context: "${config.assets.avatar.context}",
|
||||
showAccessories: true
|
||||
}
|
||||
}
|
||||
};`;
|
||||
document.getElementById('codeDisplay').textContent = code;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// Create game instances
|
||||
games.space = new DodgeGame('game1', CONFIGS.space);
|
||||
games.nature = new DodgeGame('game2', CONFIGS.nature);
|
||||
games.spooky = new DodgeGame('game3', CONFIGS.spooky);
|
||||
games.retro = new DodgeGame('game4', CONFIGS.retro);
|
||||
|
||||
// Initial render
|
||||
Object.values(games).forEach(g => g.render());
|
||||
|
||||
// Auto-start after a moment
|
||||
setTimeout(startAll, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,533 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Sky Glide - Flappy Example</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; overflow: hidden; background: #1a1a2e; touch-action: none; }
|
||||
canvas { display: block; }
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 4px #000;
|
||||
z-index: 10;
|
||||
}
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(135deg, #87CEEB, #4682B4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 100;
|
||||
}
|
||||
#loading h1 { margin-bottom: 1rem; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
|
||||
.loading-bar {
|
||||
width: 200px;
|
||||
height: 8px;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.loading-bar-fill {
|
||||
height: 100%;
|
||||
background: white;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
#startScreen {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 50;
|
||||
}
|
||||
#startScreen h2 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
#startScreen p { font-size: 1.25rem; opacity: 0.9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<h1>Sky Glide</h1>
|
||||
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
|
||||
<p id="loadingText">Loading assets...</p>
|
||||
</div>
|
||||
<div id="startScreen">
|
||||
<h2>Tap or Press Space to Start</h2>
|
||||
<p>Avoid the pipes!</p>
|
||||
</div>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div id="ui"><span id="score">0</span></div>
|
||||
|
||||
<!-- Asset Libraries -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/sky.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
||||
<script src="/assets/music/moodCategories.js"></script>
|
||||
<script src="/assets/music/musicEngine.js"></script>
|
||||
<script src="/assets/music/library/songs1-80.js"></script>
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// FLAPPY EXAMPLE - Head Only Avatar Demo
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Demonstrates: HEAD_ONLY avatar, flappy-style context,
|
||||
// sky/clouds background, Playful music mood
|
||||
|
||||
const GAME_CONFIG = {
|
||||
title: "Sky Glide",
|
||||
assets: {
|
||||
music: { mood: "Playful", energy: 5, enabled: true },
|
||||
background: { theme: "sky", variant: "clouds", animated: true },
|
||||
avatarContext: { mode: "HEAD_ONLY", context: "flappy-style", showAccessories: true }
|
||||
},
|
||||
gameplay: {
|
||||
gravity: 1200,
|
||||
flapForce: 400,
|
||||
pipeSpeed: 200,
|
||||
pipeGap: 180,
|
||||
pipeWidth: 70,
|
||||
pipeSpawnRate: 1800
|
||||
}
|
||||
};
|
||||
|
||||
const playerAvatar = {
|
||||
faceShape: 'round',
|
||||
skinTone: 'light',
|
||||
hairStyle: 'spiky',
|
||||
hairColor: '#FFD700',
|
||||
eyeShape: 'round',
|
||||
eyeColor: '#3498db',
|
||||
noseShape: 'small',
|
||||
mouthShape: 'smile'
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SETUP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let state = {
|
||||
playerY: 0,
|
||||
playerVelY: 0,
|
||||
playerRotation: 0,
|
||||
animTime: 0,
|
||||
|
||||
score: 0,
|
||||
highScore: parseInt(localStorage.getItem('skyglide_high') || '0'),
|
||||
gameOver: false,
|
||||
started: false,
|
||||
paused: false,
|
||||
lastTime: 0,
|
||||
|
||||
pipes: [],
|
||||
lastPipeTime: 0,
|
||||
clouds: []
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// LOADING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateLoading(progress, text) {
|
||||
document.getElementById('loadingFill').style.width = progress + '%';
|
||||
document.getElementById('loadingText').textContent = text;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('startScreen').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function loadAssets() {
|
||||
updateLoading(25, 'Loading sky...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(50, 'Loading music...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(75, 'Loading avatar...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(100, 'Ready!');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
hideLoading();
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOGIC
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initGame() {
|
||||
state.playerY = canvas.height / 2;
|
||||
state.playerVelY = 0;
|
||||
state.playerRotation = 0;
|
||||
state.pipes = [];
|
||||
state.lastPipeTime = 0;
|
||||
state.score = 0;
|
||||
state.gameOver = false;
|
||||
updateScoreDisplay();
|
||||
|
||||
// Initialize clouds for parallax
|
||||
state.clouds = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
state.clouds.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height * 0.6,
|
||||
size: 50 + Math.random() * 100,
|
||||
speed: 20 + Math.random() * 30
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function flap() {
|
||||
if (state.gameOver) {
|
||||
state.score = 0;
|
||||
state.gameOver = false;
|
||||
state.started = false;
|
||||
document.getElementById('startScreen').style.display = 'flex';
|
||||
initGame();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.started) {
|
||||
state.started = true;
|
||||
document.getElementById('startScreen').style.display = 'none';
|
||||
|
||||
// Start music
|
||||
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
|
||||
try {
|
||||
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
state.playerVelY = -GAME_CONFIG.gameplay.flapForce;
|
||||
}
|
||||
|
||||
function updateGame(dt) {
|
||||
if (!state.started || state.gameOver) {
|
||||
// Bob animation when not playing
|
||||
state.playerY = canvas.height / 2 + Math.sin(state.animTime / 300) * 20;
|
||||
state.animTime += dt * 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
const { gravity, pipeSpeed, pipeGap, pipeWidth, pipeSpawnRate } = GAME_CONFIG.gameplay;
|
||||
|
||||
// Apply gravity
|
||||
state.playerVelY += gravity * dt;
|
||||
state.playerY += state.playerVelY * dt;
|
||||
|
||||
// Rotation based on velocity
|
||||
state.playerRotation = Math.min(Math.max(state.playerVelY / 10, -30), 90);
|
||||
|
||||
// Spawn pipes
|
||||
state.lastPipeTime += dt * 1000;
|
||||
if (state.lastPipeTime > pipeSpawnRate) {
|
||||
state.lastPipeTime = 0;
|
||||
const gapY = 150 + Math.random() * (canvas.height - 350);
|
||||
|
||||
state.pipes.push({
|
||||
x: canvas.width + pipeWidth,
|
||||
gapY: gapY,
|
||||
passed: false
|
||||
});
|
||||
}
|
||||
|
||||
// Update pipes
|
||||
state.pipes = state.pipes.filter(pipe => {
|
||||
pipe.x -= pipeSpeed * dt;
|
||||
|
||||
// Score when passing
|
||||
if (!pipe.passed && pipe.x + pipeWidth < canvas.width / 3) {
|
||||
pipe.passed = true;
|
||||
state.score++;
|
||||
updateScoreDisplay();
|
||||
}
|
||||
|
||||
return pipe.x > -pipeWidth;
|
||||
});
|
||||
|
||||
// Update clouds
|
||||
state.clouds.forEach(cloud => {
|
||||
cloud.x -= cloud.speed * dt;
|
||||
if (cloud.x + cloud.size < 0) {
|
||||
cloud.x = canvas.width + cloud.size;
|
||||
cloud.y = Math.random() * canvas.height * 0.6;
|
||||
}
|
||||
});
|
||||
|
||||
// Collision detection
|
||||
const playerX = canvas.width / 3;
|
||||
const playerSize = 32;
|
||||
|
||||
// Ground/ceiling
|
||||
if (state.playerY < playerSize || state.playerY > canvas.height - playerSize) {
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
// Pipes
|
||||
state.pipes.forEach(pipe => {
|
||||
const pipeLeft = pipe.x;
|
||||
const pipeRight = pipe.x + pipeWidth;
|
||||
|
||||
if (playerX + playerSize > pipeLeft && playerX - playerSize < pipeRight) {
|
||||
// Check top pipe
|
||||
if (state.playerY - playerSize < pipe.gapY - pipeGap / 2) {
|
||||
endGame();
|
||||
}
|
||||
// Check bottom pipe
|
||||
if (state.playerY + playerSize > pipe.gapY + pipeGap / 2) {
|
||||
endGame();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.animTime += dt * 1000;
|
||||
}
|
||||
|
||||
function renderGame() {
|
||||
// Sky gradient background
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
gradient.addColorStop(0, '#87CEEB');
|
||||
gradient.addColorStop(0.5, '#B0E0E6');
|
||||
gradient.addColorStop(1, '#E0F7FA');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Clouds (parallax)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
state.clouds.forEach(cloud => {
|
||||
drawCloud(cloud.x, cloud.y, cloud.size);
|
||||
});
|
||||
|
||||
// Pipes
|
||||
state.pipes.forEach(pipe => {
|
||||
const { pipeWidth, pipeGap } = GAME_CONFIG.gameplay;
|
||||
const topHeight = pipe.gapY - pipeGap / 2;
|
||||
const bottomY = pipe.gapY + pipeGap / 2;
|
||||
|
||||
// Top pipe
|
||||
ctx.fillStyle = '#2ECC71';
|
||||
ctx.fillRect(pipe.x, 0, pipeWidth, topHeight);
|
||||
ctx.fillStyle = '#27AE60';
|
||||
ctx.fillRect(pipe.x - 5, topHeight - 30, pipeWidth + 10, 30);
|
||||
|
||||
// Bottom pipe
|
||||
ctx.fillStyle = '#2ECC71';
|
||||
ctx.fillRect(pipe.x, bottomY, pipeWidth, canvas.height - bottomY);
|
||||
ctx.fillStyle = '#27AE60';
|
||||
ctx.fillRect(pipe.x - 5, bottomY, pipeWidth + 10, 30);
|
||||
|
||||
// Pipe highlights
|
||||
ctx.fillStyle = '#58D68D';
|
||||
ctx.fillRect(pipe.x + 5, 0, 10, topHeight - 30);
|
||||
ctx.fillRect(pipe.x + 5, bottomY + 30, 10, canvas.height - bottomY - 30);
|
||||
});
|
||||
|
||||
// Ground
|
||||
ctx.fillStyle = '#DEB887';
|
||||
ctx.fillRect(0, canvas.height - 30, canvas.width, 30);
|
||||
ctx.fillStyle = '#8B4513';
|
||||
ctx.fillRect(0, canvas.height - 35, canvas.width, 5);
|
||||
|
||||
// Player
|
||||
const playerX = canvas.width / 3;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(playerX, state.playerY);
|
||||
ctx.rotate(state.playerRotation * Math.PI / 180);
|
||||
|
||||
if (window.ContextRenderer) {
|
||||
// Use context renderer for flappy style
|
||||
ctx.translate(-40, -40);
|
||||
window.ContextRenderer.render(
|
||||
ctx,
|
||||
GAME_CONFIG.assets.avatarContext.context,
|
||||
playerAvatar,
|
||||
40, 40,
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
} else if (window.AvatarSystem) {
|
||||
ctx.translate(-32, -32);
|
||||
window.AvatarSystem.render(
|
||||
ctx,
|
||||
playerAvatar,
|
||||
'HEAD_ONLY',
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
} else {
|
||||
// Fallback bird
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 30, 25, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Eye
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.beginPath();
|
||||
ctx.arc(10, -5, 10, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.beginPath();
|
||||
ctx.arc(12, -5, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Beak
|
||||
ctx.fillStyle = '#FF6B00';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(25, 0);
|
||||
ctx.lineTo(40, 5);
|
||||
ctx.lineTo(25, 10);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Wing
|
||||
ctx.fillStyle = '#FFA500';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-5, 5, 15, 10, Math.sin(state.animTime / 50) * 0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Game over overlay
|
||||
if (state.gameOver) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 48px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Game Over', canvas.width / 2, canvas.height / 2 - 60);
|
||||
|
||||
ctx.font = '32px Arial';
|
||||
ctx.fillText(`Score: ${state.score}`, canvas.width / 2, canvas.height / 2);
|
||||
ctx.fillText(`Best: ${state.highScore}`, canvas.width / 2, canvas.height / 2 + 40);
|
||||
|
||||
ctx.font = '24px Arial';
|
||||
ctx.fillText('Tap to Restart', canvas.width / 2, canvas.height / 2 + 100);
|
||||
}
|
||||
}
|
||||
|
||||
function drawCloud(x, y, size) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, size * 0.5, 0, Math.PI * 2);
|
||||
ctx.arc(x + size * 0.35, y - size * 0.15, size * 0.4, 0, Math.PI * 2);
|
||||
ctx.arc(x + size * 0.6, y, size * 0.35, 0, Math.PI * 2);
|
||||
ctx.arc(x + size * 0.3, y + size * 0.1, size * 0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateScoreDisplay() {
|
||||
document.getElementById('score').textContent = state.score;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.gameOver = true;
|
||||
if (state.score > state.highScore) {
|
||||
state.highScore = state.score;
|
||||
localStorage.setItem('skyglide_high', state.highScore);
|
||||
}
|
||||
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INPUT
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
flap();
|
||||
}
|
||||
if (e.code === 'Escape') {
|
||||
state.paused = !state.paused;
|
||||
if (!state.paused) {
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('click', flap);
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
flap();
|
||||
}, { passive: false });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOOP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function gameLoop(time) {
|
||||
if (state.paused) return;
|
||||
|
||||
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
|
||||
state.lastTime = time;
|
||||
|
||||
updateGame(dt);
|
||||
renderGame();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
window.pauseGame = () => { state.paused = true; };
|
||||
window.resumeGame = () => {
|
||||
state.paused = false;
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
loadAssets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,533 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Forest Jumper - Platformer Example</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; overflow: hidden; background: #1a1a2e; touch-action: none; }
|
||||
canvas { display: block; }
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
text-shadow: 2px 2px 4px #000;
|
||||
z-index: 10;
|
||||
}
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 100;
|
||||
}
|
||||
#loading h1 { margin-bottom: 1rem; }
|
||||
.loading-bar {
|
||||
width: 200px;
|
||||
height: 8px;
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.loading-bar-fill {
|
||||
height: 100%;
|
||||
background: #6366f1;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<h1>Forest Jumper</h1>
|
||||
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
|
||||
<p id="loadingText">Loading assets...</p>
|
||||
</div>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div id="ui">
|
||||
<div>Score: <span id="score">0</span></div>
|
||||
<div>Coins: <span id="coins">0</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Libraries -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/nature.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
||||
<script src="/assets/music/moodCategories.js"></script>
|
||||
<script src="/assets/music/musicEngine.js"></script>
|
||||
<script src="/assets/music/library/songs1-80.js"></script>
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
<script src="/assets/avatar/contexts/pure.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// PLATFORMER EXAMPLE - Full Body Avatar Demo
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Demonstrates: FULL_BODY avatar, platformer-standard context,
|
||||
// nature/forest background, Playful music mood
|
||||
|
||||
const GAME_CONFIG = {
|
||||
title: "Forest Jumper",
|
||||
assets: {
|
||||
music: { mood: "Playful", energy: 6, enabled: true },
|
||||
background: { theme: "nature", variant: "forest", animated: true },
|
||||
avatarContext: { mode: "FULL_BODY", context: "platformer-standard", showAccessories: true }
|
||||
},
|
||||
gameplay: {
|
||||
gravity: 1800,
|
||||
jumpForce: 650,
|
||||
moveSpeed: 280,
|
||||
platformCount: 8,
|
||||
coinSpawnRate: 2000
|
||||
}
|
||||
};
|
||||
|
||||
// Player avatar configuration
|
||||
const playerAvatar = {
|
||||
faceShape: 'round',
|
||||
skinTone: 'medium',
|
||||
hairStyle: 'short',
|
||||
hairColor: '#3d2314',
|
||||
eyeShape: 'round',
|
||||
eyeColor: '#4a7c59',
|
||||
noseShape: 'small',
|
||||
mouthShape: 'smile'
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SETUP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let state = {
|
||||
playerX: 100,
|
||||
playerY: 300,
|
||||
playerVelX: 0,
|
||||
playerVelY: 0,
|
||||
isGrounded: false,
|
||||
facingRight: true,
|
||||
currentAnim: 'idle',
|
||||
animTime: 0,
|
||||
|
||||
score: 0,
|
||||
coins: 0,
|
||||
gameOver: false,
|
||||
paused: false,
|
||||
lastTime: 0,
|
||||
|
||||
platforms: [],
|
||||
collectedCoins: [],
|
||||
particles: []
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// LOADING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let assetsLoaded = false;
|
||||
|
||||
function updateLoading(progress, text) {
|
||||
document.getElementById('loadingFill').style.width = progress + '%';
|
||||
document.getElementById('loadingText').textContent = text;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadAssets() {
|
||||
updateLoading(20, 'Loading backgrounds...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(40, 'Loading music...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(60, 'Loading avatars...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(80, 'Initializing game...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(100, 'Ready!');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
assetsLoaded = true;
|
||||
hideLoading();
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
// Start music
|
||||
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
|
||||
try {
|
||||
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
|
||||
} catch(e) { console.log('Music not available'); }
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOGIC
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initGame() {
|
||||
state.playerX = 100;
|
||||
state.playerY = canvas.height - 200;
|
||||
|
||||
// Generate platforms
|
||||
state.platforms = [];
|
||||
const groundY = canvas.height - 50;
|
||||
|
||||
// Ground
|
||||
state.platforms.push({ x: 0, y: groundY, w: canvas.width, h: 50, isGround: true });
|
||||
|
||||
// Floating platforms
|
||||
for (let i = 0; i < GAME_CONFIG.gameplay.platformCount; i++) {
|
||||
state.platforms.push({
|
||||
x: 100 + i * (canvas.width / GAME_CONFIG.gameplay.platformCount),
|
||||
y: groundY - 100 - Math.random() * 250,
|
||||
w: 100 + Math.random() * 80,
|
||||
h: 20
|
||||
});
|
||||
}
|
||||
|
||||
// Add coins on platforms
|
||||
state.collectedCoins = [];
|
||||
state.platforms.forEach((p, i) => {
|
||||
if (!p.isGround && Math.random() > 0.3) {
|
||||
state.collectedCoins.push({
|
||||
x: p.x + p.w / 2,
|
||||
y: p.y - 30,
|
||||
collected: false,
|
||||
bounce: Math.random() * Math.PI * 2
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateGame(dt) {
|
||||
const { gravity, jumpForce, moveSpeed } = GAME_CONFIG.gameplay;
|
||||
|
||||
// Horizontal movement
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) {
|
||||
state.playerVelX = -moveSpeed;
|
||||
state.facingRight = false;
|
||||
if (state.isGrounded) state.currentAnim = 'walk';
|
||||
} else if (keys['ArrowRight'] || keys['KeyD']) {
|
||||
state.playerVelX = moveSpeed;
|
||||
state.facingRight = true;
|
||||
if (state.isGrounded) state.currentAnim = 'walk';
|
||||
} else {
|
||||
state.playerVelX *= 0.85;
|
||||
if (state.isGrounded && Math.abs(state.playerVelX) < 10) {
|
||||
state.currentAnim = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
// Jump
|
||||
if ((keys['Space'] || keys['ArrowUp'] || keys['KeyW']) && state.isGrounded) {
|
||||
state.playerVelY = -jumpForce;
|
||||
state.isGrounded = false;
|
||||
state.currentAnim = 'jump';
|
||||
}
|
||||
|
||||
// Apply gravity
|
||||
state.playerVelY += gravity * dt;
|
||||
|
||||
// Move player
|
||||
state.playerX += state.playerVelX * dt;
|
||||
state.playerY += state.playerVelY * dt;
|
||||
|
||||
// Platform collision
|
||||
state.isGrounded = false;
|
||||
const playerRect = {
|
||||
x: state.playerX - 20,
|
||||
y: state.playerY - 80,
|
||||
w: 40,
|
||||
h: 80
|
||||
};
|
||||
|
||||
state.platforms.forEach(p => {
|
||||
// Check if player is above platform and falling
|
||||
if (state.playerVelY >= 0 &&
|
||||
playerRect.x + playerRect.w > p.x &&
|
||||
playerRect.x < p.x + p.w &&
|
||||
playerRect.y + playerRect.h >= p.y &&
|
||||
playerRect.y + playerRect.h <= p.y + p.h + state.playerVelY * dt + 5) {
|
||||
|
||||
state.playerY = p.y - 80;
|
||||
state.playerVelY = 0;
|
||||
state.isGrounded = true;
|
||||
if (state.currentAnim === 'jump') state.currentAnim = 'idle';
|
||||
}
|
||||
});
|
||||
|
||||
// Animation when in air
|
||||
if (!state.isGrounded) {
|
||||
state.currentAnim = state.playerVelY < 0 ? 'jump' : 'jump';
|
||||
}
|
||||
|
||||
// Screen bounds
|
||||
if (state.playerX < 30) state.playerX = 30;
|
||||
if (state.playerX > canvas.width - 30) state.playerX = canvas.width - 30;
|
||||
|
||||
// Fall death
|
||||
if (state.playerY > canvas.height + 100) {
|
||||
endGame();
|
||||
}
|
||||
|
||||
// Coin collection
|
||||
state.collectedCoins.forEach(coin => {
|
||||
if (!coin.collected) {
|
||||
coin.bounce += dt * 3;
|
||||
const coinY = coin.y + Math.sin(coin.bounce) * 5;
|
||||
const dx = state.playerX - coin.x;
|
||||
const dy = (state.playerY - 40) - coinY;
|
||||
if (Math.sqrt(dx*dx + dy*dy) < 35) {
|
||||
coin.collected = true;
|
||||
state.coins++;
|
||||
state.score += 10;
|
||||
updateScore(0);
|
||||
|
||||
// Particle effect
|
||||
for (let i = 0; i < 8; i++) {
|
||||
state.particles.push({
|
||||
x: coin.x,
|
||||
y: coinY,
|
||||
vx: (Math.random() - 0.5) * 200,
|
||||
vy: -Math.random() * 150 - 50,
|
||||
life: 1,
|
||||
color: '#FFD700'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update particles
|
||||
state.particles = state.particles.filter(p => {
|
||||
p.x += p.vx * dt;
|
||||
p.y += p.vy * dt;
|
||||
p.vy += 400 * dt;
|
||||
p.life -= dt * 2;
|
||||
return p.life > 0;
|
||||
});
|
||||
|
||||
// Score over time
|
||||
state.score += Math.floor(dt * 5);
|
||||
updateScore(0);
|
||||
|
||||
state.animTime += dt * 1000;
|
||||
}
|
||||
|
||||
function renderGame() {
|
||||
// Background
|
||||
if (window.BackgroundEngine) {
|
||||
window.BackgroundEngine.render(
|
||||
ctx,
|
||||
GAME_CONFIG.assets.background.theme,
|
||||
GAME_CONFIG.assets.background.variant,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
state.animTime
|
||||
);
|
||||
} else {
|
||||
ctx.fillStyle = '#228B22';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// Platforms
|
||||
state.platforms.forEach(p => {
|
||||
if (p.isGround) {
|
||||
ctx.fillStyle = '#3d2817';
|
||||
ctx.fillRect(p.x, p.y, p.w, p.h);
|
||||
ctx.fillStyle = '#4a7c59';
|
||||
ctx.fillRect(p.x, p.y, p.w, 10);
|
||||
} else {
|
||||
// Floating platform
|
||||
ctx.fillStyle = '#5d4037';
|
||||
ctx.fillRect(p.x, p.y, p.w, p.h);
|
||||
ctx.fillStyle = '#795548';
|
||||
ctx.fillRect(p.x + 2, p.y + 2, p.w - 4, 6);
|
||||
}
|
||||
});
|
||||
|
||||
// Coins
|
||||
state.collectedCoins.forEach(coin => {
|
||||
if (!coin.collected) {
|
||||
const coinY = coin.y + Math.sin(coin.bounce) * 5;
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.beginPath();
|
||||
ctx.arc(coin.x, coinY, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#FFA500';
|
||||
ctx.beginPath();
|
||||
ctx.arc(coin.x - 3, coinY - 3, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Particles
|
||||
state.particles.forEach(p => {
|
||||
ctx.globalAlpha = p.life;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.fillRect(p.x - 3, p.y - 3, 6, 6);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Player avatar
|
||||
ctx.save();
|
||||
ctx.translate(state.playerX, state.playerY);
|
||||
if (!state.facingRight) {
|
||||
ctx.scale(-1, 1);
|
||||
}
|
||||
ctx.translate(-32, -160);
|
||||
|
||||
if (window.AvatarSystem) {
|
||||
window.AvatarSystem.render(
|
||||
ctx,
|
||||
playerAvatar,
|
||||
GAME_CONFIG.assets.avatarContext.mode,
|
||||
state.currentAnim,
|
||||
state.animTime
|
||||
);
|
||||
} else {
|
||||
// Fallback
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
ctx.fillRect(12, 100, 40, 60);
|
||||
ctx.fillStyle = '#FFCC80';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 80, 25, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateScore(points) {
|
||||
document.getElementById('score').textContent = state.score;
|
||||
document.getElementById('coins').textContent = state.coins;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.gameOver = true;
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (confirm(`Game Over!\nScore: ${state.score}\nCoins: ${state.coins}\n\nPlay again?`)) {
|
||||
state.score = 0;
|
||||
state.coins = 0;
|
||||
state.gameOver = false;
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INPUT
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
keys[e.code] = true;
|
||||
if (e.code === 'Escape') {
|
||||
state.paused = !state.paused;
|
||||
if (!state.paused) {
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
keys[e.code] = false;
|
||||
});
|
||||
|
||||
// Touch controls
|
||||
let touchStartX = 0;
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
touchStartX = e.touches[0].clientX;
|
||||
const y = e.touches[0].clientY;
|
||||
|
||||
// Top half = jump
|
||||
if (y < canvas.height / 2) {
|
||||
keys['Space'] = true;
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
const x = e.touches[0].clientX;
|
||||
const dx = x - touchStartX;
|
||||
|
||||
keys['ArrowLeft'] = dx < -30;
|
||||
keys['ArrowRight'] = dx > 30;
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
keys['ArrowLeft'] = false;
|
||||
keys['ArrowRight'] = false;
|
||||
keys['Space'] = false;
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOOP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function gameLoop(time) {
|
||||
if (state.gameOver || state.paused) return;
|
||||
|
||||
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
|
||||
state.lastTime = time;
|
||||
|
||||
updateGame(dt);
|
||||
renderGame();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Parent window controls
|
||||
window.pauseGame = () => { state.paused = true; };
|
||||
window.resumeGame = () => {
|
||||
state.paused = false;
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
// Start loading
|
||||
loadAssets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,702 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Night Racer - Racing Example</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; overflow: hidden; background: #0a0a15; touch-action: none; }
|
||||
canvas { display: block; }
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
text-shadow: 0 0 10px #0ff;
|
||||
z-index: 10;
|
||||
}
|
||||
#ui div { margin-bottom: 5px; }
|
||||
#speedometer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
color: #0ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 20px #0ff;
|
||||
}
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(135deg, #0a0a15, #1a1a2e);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #0ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
z-index: 100;
|
||||
}
|
||||
#loading h1 {
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 0 20px #0ff;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.loading-bar {
|
||||
width: 250px;
|
||||
height: 4px;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #0ff;
|
||||
}
|
||||
.loading-bar-fill {
|
||||
height: 100%;
|
||||
background: #0ff;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
box-shadow: 0 0 10px #0ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<h1>NIGHT RACER</h1>
|
||||
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
|
||||
<p id="loadingText">Initializing...</p>
|
||||
</div>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div id="ui">
|
||||
<div>SCORE: <span id="score">0</span></div>
|
||||
<div>DISTANCE: <span id="distance">0</span>m</div>
|
||||
</div>
|
||||
<div id="speedometer"><span id="speed">0</span> km/h</div>
|
||||
|
||||
<!-- Asset Libraries -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/urban.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
||||
<script src="/assets/music/moodCategories.js"></script>
|
||||
<script src="/assets/music/musicEngine.js"></script>
|
||||
<script src="/assets/music/library/songs1-80.js"></script>
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RACER EXAMPLE - Vehicle Context Demo
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Demonstrates: HEAD_AND_SHOULDERS avatar, race-car context,
|
||||
// urban/neonDistrict background, Intense music mood
|
||||
|
||||
const GAME_CONFIG = {
|
||||
title: "Night Racer",
|
||||
assets: {
|
||||
music: { mood: "Intense", energy: 9, enabled: true },
|
||||
background: { theme: "urban", variant: "neonDistrict", animated: true },
|
||||
avatarContext: { mode: "HEAD_AND_SHOULDERS", context: "race-car", showAccessories: true }
|
||||
},
|
||||
gameplay: {
|
||||
baseSpeed: 300,
|
||||
maxSpeed: 600,
|
||||
acceleration: 150,
|
||||
deceleration: 200,
|
||||
laneWidth: 100,
|
||||
lanes: 3,
|
||||
obstacleSpawnRate: 1200,
|
||||
coinSpawnRate: 800
|
||||
}
|
||||
};
|
||||
|
||||
const playerAvatar = {
|
||||
faceShape: 'square',
|
||||
skinTone: 'tan',
|
||||
hairStyle: 'short',
|
||||
hairColor: '#1a1a1a',
|
||||
eyeShape: 'sharp',
|
||||
eyeColor: '#2ecc71',
|
||||
noseShape: 'pointed',
|
||||
mouthShape: 'serious'
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SETUP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let state = {
|
||||
playerLane: 1,
|
||||
playerX: 0,
|
||||
targetX: 0,
|
||||
speed: 0,
|
||||
distance: 0,
|
||||
animTime: 0,
|
||||
|
||||
score: 0,
|
||||
gameOver: false,
|
||||
paused: false,
|
||||
lastTime: 0,
|
||||
|
||||
obstacles: [],
|
||||
coins: [],
|
||||
roadLines: [],
|
||||
buildings: [],
|
||||
lastObstacle: 0,
|
||||
lastCoin: 0
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// LOADING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateLoading(progress, text) {
|
||||
document.getElementById('loadingFill').style.width = progress + '%';
|
||||
document.getElementById('loadingText').textContent = text;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadAssets() {
|
||||
updateLoading(20, 'Loading city...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(40, 'Loading music...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(60, 'Loading vehicle...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(80, 'Starting engine...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(100, 'GO!');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
hideLoading();
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
|
||||
try {
|
||||
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOGIC
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initGame() {
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
|
||||
state.playerLane = 1;
|
||||
state.playerX = roadX + laneWidth * state.playerLane + laneWidth / 2;
|
||||
state.targetX = state.playerX;
|
||||
state.speed = GAME_CONFIG.gameplay.baseSpeed;
|
||||
state.distance = 0;
|
||||
state.score = 0;
|
||||
state.obstacles = [];
|
||||
state.coins = [];
|
||||
state.lastObstacle = 0;
|
||||
state.lastCoin = 0;
|
||||
|
||||
// Initialize road lines
|
||||
state.roadLines = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
state.roadLines.push({
|
||||
y: i * 80,
|
||||
lane: 0
|
||||
});
|
||||
state.roadLines.push({
|
||||
y: i * 80,
|
||||
lane: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize buildings
|
||||
state.buildings = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
state.buildings.push({
|
||||
x: Math.random() > 0.5 ? -50 - Math.random() * 100 : canvas.width + 50 + Math.random() * 100,
|
||||
y: i * 100 - 500,
|
||||
width: 60 + Math.random() * 100,
|
||||
height: 150 + Math.random() * 300,
|
||||
color: `hsl(${260 + Math.random() * 40}, 50%, ${15 + Math.random() * 15}%)`
|
||||
});
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function getLaneX(lane) {
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
return roadX + laneWidth * lane + laneWidth / 2;
|
||||
}
|
||||
|
||||
function updateGame(dt) {
|
||||
const { baseSpeed, maxSpeed, acceleration, deceleration, laneWidth, lanes, obstacleSpawnRate, coinSpawnRate } = GAME_CONFIG.gameplay;
|
||||
|
||||
// Speed control
|
||||
if (keys['ArrowUp'] || keys['KeyW']) {
|
||||
state.speed = Math.min(state.speed + acceleration * dt, maxSpeed);
|
||||
} else {
|
||||
state.speed = Math.max(state.speed - deceleration * dt * 0.3, baseSpeed);
|
||||
}
|
||||
|
||||
// Lane switching
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) {
|
||||
if (!keys._leftPressed) {
|
||||
keys._leftPressed = true;
|
||||
state.playerLane = Math.max(0, state.playerLane - 1);
|
||||
state.targetX = getLaneX(state.playerLane);
|
||||
}
|
||||
} else {
|
||||
keys._leftPressed = false;
|
||||
}
|
||||
|
||||
if (keys['ArrowRight'] || keys['KeyD']) {
|
||||
if (!keys._rightPressed) {
|
||||
keys._rightPressed = true;
|
||||
state.playerLane = Math.min(lanes - 1, state.playerLane + 1);
|
||||
state.targetX = getLaneX(state.playerLane);
|
||||
}
|
||||
} else {
|
||||
keys._rightPressed = false;
|
||||
}
|
||||
|
||||
// Smooth lane movement
|
||||
state.playerX += (state.targetX - state.playerX) * 10 * dt;
|
||||
|
||||
// Update distance
|
||||
state.distance += state.speed * dt * 0.01;
|
||||
|
||||
// Spawn obstacles
|
||||
state.lastObstacle += dt * 1000;
|
||||
if (state.lastObstacle > obstacleSpawnRate * (baseSpeed / state.speed)) {
|
||||
state.lastObstacle = 0;
|
||||
const lane = Math.floor(Math.random() * lanes);
|
||||
state.obstacles.push({
|
||||
x: getLaneX(lane),
|
||||
y: -100,
|
||||
lane: lane,
|
||||
type: Math.random() > 0.7 ? 'truck' : 'car',
|
||||
color: `hsl(${Math.random() * 360}, 70%, 50%)`
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn coins
|
||||
state.lastCoin += dt * 1000;
|
||||
if (state.lastCoin > coinSpawnRate) {
|
||||
state.lastCoin = 0;
|
||||
const lane = Math.floor(Math.random() * lanes);
|
||||
// Don't spawn on obstacles
|
||||
const blocked = state.obstacles.some(o => o.lane === lane && o.y < 200);
|
||||
if (!blocked) {
|
||||
state.coins.push({
|
||||
x: getLaneX(lane),
|
||||
y: -50,
|
||||
lane: lane,
|
||||
rotation: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update obstacles
|
||||
state.obstacles = state.obstacles.filter(obs => {
|
||||
obs.y += state.speed * dt;
|
||||
|
||||
// Collision check
|
||||
const playerY = canvas.height - 150;
|
||||
if (Math.abs(obs.x - state.playerX) < 40 &&
|
||||
Math.abs(obs.y - playerY) < 60) {
|
||||
endGame();
|
||||
}
|
||||
|
||||
return obs.y < canvas.height + 150;
|
||||
});
|
||||
|
||||
// Update coins
|
||||
state.coins = state.coins.filter(coin => {
|
||||
coin.y += state.speed * dt;
|
||||
coin.rotation += dt * 5;
|
||||
|
||||
// Collection check
|
||||
const playerY = canvas.height - 150;
|
||||
if (Math.abs(coin.x - state.playerX) < 50 &&
|
||||
Math.abs(coin.y - playerY) < 50) {
|
||||
state.score += 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
return coin.y < canvas.height + 50;
|
||||
});
|
||||
|
||||
// Update road lines
|
||||
state.roadLines.forEach(line => {
|
||||
line.y += state.speed * dt;
|
||||
if (line.y > canvas.height) {
|
||||
line.y -= 15 * 80;
|
||||
}
|
||||
});
|
||||
|
||||
// Update buildings
|
||||
state.buildings.forEach(b => {
|
||||
b.y += state.speed * dt * 0.3;
|
||||
if (b.y > canvas.height + 200) {
|
||||
b.y -= 20 * 100 + 500;
|
||||
b.height = 150 + Math.random() * 300;
|
||||
}
|
||||
});
|
||||
|
||||
// Score over time
|
||||
state.score += Math.floor(state.speed * dt * 0.05);
|
||||
|
||||
state.animTime += dt * 1000;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function renderGame() {
|
||||
// Dark background
|
||||
ctx.fillStyle = '#0a0a15';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// City skyline (buildings)
|
||||
state.buildings.forEach(b => {
|
||||
// Building
|
||||
ctx.fillStyle = b.color;
|
||||
const bx = b.x < 0 ? b.x : b.x;
|
||||
ctx.fillRect(bx, b.y, b.width, b.height);
|
||||
|
||||
// Windows
|
||||
ctx.fillStyle = Math.random() > 0.3 ? '#ffff88' : '#333';
|
||||
for (let wy = b.y + 20; wy < b.y + b.height - 20; wy += 30) {
|
||||
for (let wx = bx + 10; wx < bx + b.width - 10; wx += 20) {
|
||||
if (Math.random() > 0.4) {
|
||||
ctx.fillRect(wx, wy, 10, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes + 40;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
|
||||
// Road
|
||||
ctx.fillStyle = '#1a1a2a';
|
||||
ctx.fillRect(roadX, 0, roadWidth, canvas.height);
|
||||
|
||||
// Road edges (neon)
|
||||
ctx.strokeStyle = '#ff00ff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.shadowColor = '#ff00ff';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(roadX, 0);
|
||||
ctx.lineTo(roadX, canvas.height);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(roadX + roadWidth, 0);
|
||||
ctx.lineTo(roadX + roadWidth, canvas.height);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Lane dividers
|
||||
ctx.strokeStyle = '#0ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.shadowColor = '#0ff';
|
||||
ctx.shadowBlur = 10;
|
||||
state.roadLines.forEach(line => {
|
||||
const x1 = roadX + 20 + laneWidth * (line.lane + 1);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, line.y);
|
||||
ctx.lineTo(x1, line.y + 40);
|
||||
ctx.stroke();
|
||||
});
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Coins
|
||||
state.coins.forEach(coin => {
|
||||
ctx.save();
|
||||
ctx.translate(coin.x, coin.y);
|
||||
ctx.rotate(coin.rotation);
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.shadowColor = '#FFD700';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#FFA500';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-4, -4, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Obstacles (other cars)
|
||||
state.obstacles.forEach(obs => {
|
||||
ctx.save();
|
||||
ctx.translate(obs.x, obs.y);
|
||||
|
||||
if (obs.type === 'truck') {
|
||||
// Truck
|
||||
ctx.fillStyle = obs.color;
|
||||
ctx.fillRect(-25, -60, 50, 100);
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-20, -55, 40, 25);
|
||||
// Wheels
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(-28, -40, 8, 20);
|
||||
ctx.fillRect(20, -40, 8, 20);
|
||||
ctx.fillRect(-28, 20, 8, 20);
|
||||
ctx.fillRect(20, 20, 8, 20);
|
||||
} else {
|
||||
// Car
|
||||
ctx.fillStyle = obs.color;
|
||||
ctx.fillRect(-20, -40, 40, 70);
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-15, -35, 30, 20);
|
||||
// Tail lights
|
||||
ctx.fillStyle = '#ff0000';
|
||||
ctx.shadowColor = '#ff0000';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.fillRect(-18, 25, 8, 5);
|
||||
ctx.fillRect(10, 25, 8, 5);
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Player car with avatar
|
||||
const playerY = canvas.height - 150;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(state.playerX, playerY);
|
||||
|
||||
// Car body
|
||||
ctx.fillStyle = '#e74c3c';
|
||||
ctx.shadowColor = '#e74c3c';
|
||||
ctx.shadowBlur = 20;
|
||||
|
||||
// Main body
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-30, 50);
|
||||
ctx.lineTo(-35, 20);
|
||||
ctx.lineTo(-30, -30);
|
||||
ctx.lineTo(-20, -50);
|
||||
ctx.lineTo(20, -50);
|
||||
ctx.lineTo(30, -30);
|
||||
ctx.lineTo(35, 20);
|
||||
ctx.lineTo(30, 50);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Windshield
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, -45);
|
||||
ctx.lineTo(-25, -15);
|
||||
ctx.lineTo(25, -15);
|
||||
ctx.lineTo(20, -45);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Avatar in car (head and shoulders visible)
|
||||
if (window.ContextRenderer) {
|
||||
ctx.save();
|
||||
ctx.translate(-32, -65);
|
||||
window.ContextRenderer.render(
|
||||
ctx,
|
||||
GAME_CONFIG.assets.avatarContext.context,
|
||||
playerAvatar,
|
||||
32, 32,
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
ctx.restore();
|
||||
} else if (window.AvatarSystem) {
|
||||
ctx.save();
|
||||
ctx.translate(-32, -65);
|
||||
window.AvatarSystem.render(
|
||||
ctx,
|
||||
playerAvatar,
|
||||
'HEAD_AND_SHOULDERS',
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Headlights
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.shadowColor = '#fff';
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-18, -45, 5, 0, Math.PI * 2);
|
||||
ctx.arc(18, -45, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Light beams
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, -50);
|
||||
ctx.lineTo(-40, -200);
|
||||
ctx.lineTo(0, -200);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, -50);
|
||||
ctx.lineTo(0, -200);
|
||||
ctx.lineTo(40, -200);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Speed lines effect at high speed
|
||||
if (state.speed > 400) {
|
||||
ctx.strokeStyle = `rgba(0, 255, 255, ${(state.speed - 400) / 400})`;
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x, y + 50 + state.speed * 0.2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateUI() {
|
||||
document.getElementById('score').textContent = state.score;
|
||||
document.getElementById('distance').textContent = Math.floor(state.distance);
|
||||
document.getElementById('speed').textContent = Math.floor(state.speed * 0.6);
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.gameOver = true;
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (confirm(`GAME OVER\n\nScore: ${state.score}\nDistance: ${Math.floor(state.distance)}m\n\nRace again?`)) {
|
||||
state.gameOver = false;
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INPUT
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
keys[e.code] = true;
|
||||
if (e.code === 'Escape') {
|
||||
state.paused = !state.paused;
|
||||
if (!state.paused) {
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
keys[e.code] = false;
|
||||
});
|
||||
|
||||
// Touch controls
|
||||
let touchStartX = 0;
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
touchStartX = e.touches[0].clientX;
|
||||
keys['ArrowUp'] = true;
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
const x = e.touches[0].clientX;
|
||||
const dx = x - touchStartX;
|
||||
|
||||
if (dx < -50) {
|
||||
keys['ArrowLeft'] = true;
|
||||
keys['ArrowRight'] = false;
|
||||
touchStartX = x;
|
||||
} else if (dx > 50) {
|
||||
keys['ArrowRight'] = true;
|
||||
keys['ArrowLeft'] = false;
|
||||
touchStartX = x;
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
keys['ArrowUp'] = false;
|
||||
keys['ArrowLeft'] = false;
|
||||
keys['ArrowRight'] = false;
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOOP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function gameLoop(time) {
|
||||
if (state.gameOver || state.paused) return;
|
||||
|
||||
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
|
||||
state.lastTime = time;
|
||||
|
||||
updateGame(dt);
|
||||
renderGame();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
window.pauseGame = () => { state.paused = true; };
|
||||
window.resumeGame = () => {
|
||||
state.paused = false;
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
loadAssets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
This is a placeholder - we'll create a proper icon
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#6366f1"/>
|
||||
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="96" fill="url(#bg)"/>
|
||||
<text x="256" y="340" font-family="Arial, sans-serif" font-size="280" text-anchor="middle" fill="white">🎮</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 461 B |
@@ -0,0 +1,56 @@
|
||||
// Push notification handler for GamerComp PWA
|
||||
// This file is loaded by the service worker via importScripts
|
||||
|
||||
self.addEventListener('push', function(event) {
|
||||
if (!event.data) return;
|
||||
|
||||
try {
|
||||
const data = event.data.json();
|
||||
const options = {
|
||||
body: data.body || 'You have a new notification',
|
||||
icon: '/icon.svg',
|
||||
badge: '/icon.svg',
|
||||
vibrate: [100, 50, 100],
|
||||
data: {
|
||||
url: data.url || '/'
|
||||
},
|
||||
actions: [
|
||||
{ action: 'open', title: 'Open' }
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title || 'GamerComp', options)
|
||||
);
|
||||
} catch (err) {
|
||||
// Fallback for non-JSON payloads
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('GamerComp', {
|
||||
body: event.data.text(),
|
||||
icon: '/icon.svg'
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
event.notification.close();
|
||||
|
||||
const url = event.notification.data?.url || '/';
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
|
||||
// Try to focus an existing window
|
||||
for (const client of clientList) {
|
||||
if (client.url.includes('gamercomp.com') && 'focus' in client) {
|
||||
client.navigate(url);
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
// Open new window if none exists
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,451 @@
|
||||
# Game Template Guide for Haiku
|
||||
|
||||
This guide explains how to use the game templates to generate HTML5 canvas games for GamerComp.
|
||||
|
||||
## Template Selection
|
||||
|
||||
### `gameTemplateAssets.html` - Full Asset Library Template
|
||||
Use this template when the game needs:
|
||||
- Procedural background themes (120 options)
|
||||
- Procedural music (160 songs, 8 moods)
|
||||
- User's avatar with game contexts (26 contexts)
|
||||
- Accessories and effects
|
||||
|
||||
**Best for:** RPGs, adventure games, racing games, space shooters, platformers, any game where the player's avatar matters.
|
||||
|
||||
### `gameTemplateSimple.html` - Minimal Template
|
||||
Use this template when the game needs:
|
||||
- Simple graphics (shapes, basic sprites)
|
||||
- No avatar/user identity
|
||||
- Fast loading (no asset libraries)
|
||||
|
||||
**Best for:** Puzzle games, simple arcade games, physics toys, creative tools, games where the player controls an object (not an avatar).
|
||||
|
||||
---
|
||||
|
||||
## Using gameTemplateAssets.html
|
||||
|
||||
### Step 1: Fill in GAME_CONFIG
|
||||
|
||||
Replace the placeholder comments with actual values:
|
||||
|
||||
```javascript
|
||||
const GAME_CONFIG = {
|
||||
title: "Space Defender", // Replace <!-- GAME_TITLE -->
|
||||
|
||||
assets: {
|
||||
music: {
|
||||
mood: "Epic", // Replace <!-- MUSIC_MOOD -->
|
||||
energy: 8,
|
||||
enabled: true
|
||||
},
|
||||
|
||||
background: {
|
||||
theme: "space", // Replace <!-- BG_THEME -->
|
||||
variant: "nebula", // Replace <!-- BG_VARIANT -->
|
||||
animated: true,
|
||||
effects: ["stars"]
|
||||
},
|
||||
|
||||
avatarContext: {
|
||||
mode: "HEAD_ONLY", // Replace <!-- AVATAR_MODE -->
|
||||
context: "spaceship-cockpit", // Replace <!-- CONTEXT_TYPE -->
|
||||
showAccessories: true
|
||||
}
|
||||
},
|
||||
|
||||
gameplay: {
|
||||
// Add your game-specific config
|
||||
enemySpeed: 200,
|
||||
spawnRate: 2000,
|
||||
bulletSpeed: 500
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Choose Asset Combinations
|
||||
|
||||
#### Music Moods (8 options)
|
||||
| Mood | Best For | Energy Range |
|
||||
|------|----------|--------------|
|
||||
| Epic | Boss battles, action games | 7-10 |
|
||||
| Chill | Puzzle games, casual games | 2-4 |
|
||||
| Intense | Racing, shooters, action | 8-10 |
|
||||
| Playful | Kids games, casual games | 5-7 |
|
||||
| Mysterious | Horror, mystery, exploration | 3-6 |
|
||||
| Heroic | Adventure, platformers | 6-8 |
|
||||
| Quirky | Puzzle, comedy, unique games | 4-7 |
|
||||
| Ambient | Background, meditation, creative | 1-3 |
|
||||
|
||||
#### Background Themes (15 themes × 8 variants each)
|
||||
| Theme | Variants | Best For |
|
||||
|-------|----------|----------|
|
||||
| space | nebula, asteroidField, deepSpace, planets, spaceStation, warpSpeed, satellites, blackHole | Space games, sci-fi |
|
||||
| nature | forest, jungle, desert, mountains, ocean, underwater, waterfall, meadow | Adventure, relaxing |
|
||||
| urban | citySkyline, street, rooftop, neonDistrict, subway, park, mall, alley | Racing, action |
|
||||
| fantasy | castle, dungeon, enchantedForest, floatingIslands, magicalLibrary, crystalCave, temple, tower | RPG, adventure |
|
||||
| abstract | geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix | Puzzle, creative |
|
||||
| retro | pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave | Arcade, retro |
|
||||
| indoor | laboratory, classroom, bedroom, kitchen, arcade, office, library, gym | Simulation, casual |
|
||||
| weather | rain, snow, storm, sunset, sunrise, fog, lightning, aurora | Atmospheric |
|
||||
| sports | stadium, raceTrack, court, field, arena, pool, gym, skatepark | Sports games |
|
||||
| seasonal | springMeadow, autumnLeaves, winterWonderland, summerBeach, harvest, blossom, snowfall, tropical | Seasonal themes |
|
||||
| underwater | coralReef, deepSea, kelpForest, submarine, shipwreck, abyss, iceCave, treasure | Ocean games |
|
||||
| sky | clouds, flyingHigh, hotAirBalloon, airplaneView, stratosphere, birdsEye, horizon, starfield | Flying games |
|
||||
| mechanical | gears, circuits, factory, robotFactory, conveyorBelts, pipes, steampunk, techLab | Tech, puzzle |
|
||||
| spooky | hauntedHouse, graveyard, darkForest, abandonedBuilding, crypt, manor, fog, shadows | Horror, Halloween |
|
||||
| colorful | rainbow, paintSplatter, candyWorld, disco, neon, tieDye, gradient, prism | Kids, casual |
|
||||
|
||||
#### Avatar Modes (4 options)
|
||||
| Mode | Dimensions | Use When |
|
||||
|------|------------|----------|
|
||||
| HEAD_ONLY | 64×64 | Cockpit view, flying games, maze games |
|
||||
| HEAD_AND_SHOULDERS | 64×96 | Driving games, submarine, conversation |
|
||||
| UPPER_BODY | 64×128 | Racing, shooting, hoverboard |
|
||||
| FULL_BODY | 64×160 | Platformers, RPGs, sports, adventure |
|
||||
|
||||
#### Avatar Contexts (26 options)
|
||||
|
||||
**Vehicle Contexts:**
|
||||
- `spaceship-cockpit` (HEAD_ONLY) - Space shooters
|
||||
- `race-car` (HEAD_AND_SHOULDERS) - Racing games
|
||||
- `airplane` (HEAD_AND_SHOULDERS) - Flying games
|
||||
- `flappy-style` (HEAD_ONLY) - Flappy bird clones
|
||||
- `tank` (HEAD_ONLY) - Tank games
|
||||
- `pacman-style` (HEAD_ONLY) - Maze games
|
||||
- `hoverboard` (UPPER_BODY) - Hoverboard games
|
||||
- `jetpack` (UPPER_BODY) - Jetpack games
|
||||
- `mech-suit` (HEAD_ONLY) - Mech combat
|
||||
- `submarine` (HEAD_AND_SHOULDERS) - Underwater
|
||||
- `dragon-rider` (FULL_BODY) - Dragon games
|
||||
- `motorcycle` (FULL_BODY) - Motorcycle racing
|
||||
|
||||
**Costume Contexts:**
|
||||
- `knight-armor` (FULL_BODY) - Medieval games
|
||||
- `space-suit` (FULL_BODY) - Space exploration
|
||||
- `ninja-outfit` (FULL_BODY) - Ninja games
|
||||
- `wizard-robes` (FULL_BODY) - Magic games
|
||||
- `superhero-cape` (FULL_BODY) - Hero games
|
||||
- `athlete-uniform` (FULL_BODY) - Sports
|
||||
- `pirate-outfit` (FULL_BODY) - Pirate games
|
||||
- `scientist-labcoat` (FULL_BODY) - Science games
|
||||
- `chef-outfit` (FULL_BODY) - Cooking games
|
||||
- `cowboy-gear` (FULL_BODY) - Western games
|
||||
|
||||
**Pure Contexts:**
|
||||
- `platformer-standard` (FULL_BODY) - Standard platformer
|
||||
- `sports-player` (FULL_BODY) - Sports games
|
||||
- `adventure-hero` (FULL_BODY) - Adventure games
|
||||
- `runner` (FULL_BODY) - Endless runners
|
||||
|
||||
### Step 3: Implement Game Functions
|
||||
|
||||
The template provides these functions for you to implement:
|
||||
|
||||
```javascript
|
||||
// Called once at start and on restart
|
||||
function initGame() {
|
||||
// Initialize enemies, items, level data
|
||||
state.enemies = [];
|
||||
state.items = [];
|
||||
}
|
||||
|
||||
// Called every frame (dt = seconds since last frame)
|
||||
function updateGame(dt) {
|
||||
// Update positions, check collisions, spawn objects
|
||||
state.enemies.forEach(e => {
|
||||
e.x += e.velX * dt;
|
||||
e.y += e.velY * dt;
|
||||
});
|
||||
|
||||
// Check player collision with enemies
|
||||
state.enemies.forEach(e => {
|
||||
if (circleCollision(
|
||||
{ x: state.playerX, y: state.playerY, r: 30 },
|
||||
{ x: e.x, y: e.y, r: e.radius }
|
||||
)) {
|
||||
updateHealth(-10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called every frame after updateGame
|
||||
function renderGame() {
|
||||
// Draw enemies, items, effects
|
||||
state.enemies.forEach(e => {
|
||||
ctx.fillStyle = e.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// Input handlers
|
||||
function handleKeyDown(code) {
|
||||
if (code === 'Space') {
|
||||
shoot(); // Your function
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart(x, y) {
|
||||
// Handle tap
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Use Provided Utilities
|
||||
|
||||
The template includes these utility functions:
|
||||
|
||||
```javascript
|
||||
// Score & Health
|
||||
updateScore(10); // Add 10 points
|
||||
updateHealth(-5); // Lose 5 health
|
||||
updateHealth(10); // Gain 10 health (capped at 100)
|
||||
|
||||
// Animation
|
||||
setPlayerAnimation('walk'); // idle, walk, run, jump, hurt, celebrate, etc.
|
||||
|
||||
// Math
|
||||
randomInt(1, 10); // Random integer 1-10
|
||||
randomFloat(0, 1); // Random float 0-1
|
||||
clamp(value, 0, 100); // Clamp to range
|
||||
lerp(0, 100, 0.5); // Linear interpolation = 50
|
||||
|
||||
// Collision
|
||||
rectCollision(r1, r2); // {x, y, width, height} objects
|
||||
circleCollision(c1, c2); // {x, y, radius} objects
|
||||
|
||||
// Game flow
|
||||
endGame(); // Trigger game over
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using gameTemplateSimple.html
|
||||
|
||||
For simple games without avatars:
|
||||
|
||||
```javascript
|
||||
const GAME_CONFIG = {
|
||||
title: "Catch the Stars",
|
||||
|
||||
background: {
|
||||
type: "gradient",
|
||||
colors: ["#0f0c29", "#302b63", "#24243e"]
|
||||
},
|
||||
|
||||
player: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
color: "#FFD700",
|
||||
speed: 400
|
||||
},
|
||||
|
||||
gameplay: {
|
||||
starSpawnRate: 1000,
|
||||
starFallSpeed: 200
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Then implement:
|
||||
- `initGame()` - Setup
|
||||
- `update(dt)` - Game logic
|
||||
- `render()` - Draw objects
|
||||
- Input handlers
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Platformer Movement
|
||||
```javascript
|
||||
const GRAVITY = 1500;
|
||||
const JUMP_FORCE = 600;
|
||||
|
||||
function update(dt) {
|
||||
// Horizontal movement
|
||||
if (keys['ArrowLeft']) state.playerVelX = -300;
|
||||
else if (keys['ArrowRight']) state.playerVelX = 300;
|
||||
else state.playerVelX *= 0.8; // Friction
|
||||
|
||||
// Apply gravity
|
||||
state.playerVelY += GRAVITY * dt;
|
||||
|
||||
// Move player
|
||||
state.playerX += state.playerVelX * dt;
|
||||
state.playerY += state.playerVelY * dt;
|
||||
|
||||
// Ground collision
|
||||
if (state.playerY > canvas.height - 50) {
|
||||
state.playerY = canvas.height - 50;
|
||||
state.playerVelY = 0;
|
||||
state.isGrounded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(code) {
|
||||
if (code === 'Space' && state.isGrounded) {
|
||||
state.playerVelY = -JUMP_FORCE;
|
||||
state.isGrounded = false;
|
||||
setPlayerAnimation('jump');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Space Shooter
|
||||
```javascript
|
||||
state.bullets = [];
|
||||
state.enemies = [];
|
||||
|
||||
function update(dt) {
|
||||
// Update bullets
|
||||
state.bullets = state.bullets.filter(b => {
|
||||
b.y -= 500 * dt;
|
||||
return b.y > 0;
|
||||
});
|
||||
|
||||
// Spawn enemies
|
||||
if (Math.random() < 0.02) {
|
||||
state.enemies.push({
|
||||
x: randomInt(50, canvas.width - 50),
|
||||
y: -30,
|
||||
radius: 20,
|
||||
color: '#ff4444'
|
||||
});
|
||||
}
|
||||
|
||||
// Update enemies
|
||||
state.enemies.forEach(e => e.y += 150 * dt);
|
||||
state.enemies = state.enemies.filter(e => e.y < canvas.height + 50);
|
||||
|
||||
// Collision: bullets vs enemies
|
||||
state.bullets.forEach(b => {
|
||||
state.enemies = state.enemies.filter(e => {
|
||||
if (circleCollision(b, e)) {
|
||||
updateScore(10);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyDown(code) {
|
||||
if (code === 'Space') {
|
||||
state.bullets.push({
|
||||
x: state.playerX,
|
||||
y: state.playerY - 30,
|
||||
radius: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Endless Runner
|
||||
```javascript
|
||||
let scrollSpeed = 300;
|
||||
let obstacles = [];
|
||||
let lastObstacle = 0;
|
||||
|
||||
function update(dt) {
|
||||
scrollSpeed += dt * 2; // Speed up over time
|
||||
|
||||
// Jump
|
||||
if (keys['Space'] && state.isGrounded) {
|
||||
state.playerVelY = -500;
|
||||
state.isGrounded = false;
|
||||
}
|
||||
|
||||
// Gravity
|
||||
state.playerVelY += 1200 * dt;
|
||||
state.playerY += state.playerVelY * dt;
|
||||
|
||||
// Ground
|
||||
const ground = canvas.height - 100;
|
||||
if (state.playerY > ground) {
|
||||
state.playerY = ground;
|
||||
state.isGrounded = true;
|
||||
}
|
||||
|
||||
// Spawn obstacles
|
||||
lastObstacle += dt * 1000;
|
||||
if (lastObstacle > 1500) {
|
||||
obstacles.push({ x: canvas.width, y: ground, w: 30, h: 50 });
|
||||
lastObstacle = 0;
|
||||
}
|
||||
|
||||
// Move obstacles
|
||||
obstacles = obstacles.filter(o => {
|
||||
o.x -= scrollSpeed * dt;
|
||||
return o.x > -50;
|
||||
});
|
||||
|
||||
// Score
|
||||
updateScore(Math.floor(dt * 10));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Combinations by Game Type
|
||||
|
||||
### Space Shooter
|
||||
```javascript
|
||||
music: { mood: "Epic", energy: 8 }
|
||||
background: { theme: "space", variant: "nebula" }
|
||||
avatarContext: { mode: "HEAD_ONLY", context: "spaceship-cockpit" }
|
||||
```
|
||||
|
||||
### Platformer
|
||||
```javascript
|
||||
music: { mood: "Playful", energy: 6 }
|
||||
background: { theme: "nature", variant: "forest" }
|
||||
avatarContext: { mode: "FULL_BODY", context: "platformer-standard" }
|
||||
```
|
||||
|
||||
### Racing
|
||||
```javascript
|
||||
music: { mood: "Intense", energy: 9 }
|
||||
background: { theme: "urban", variant: "street" }
|
||||
avatarContext: { mode: "HEAD_AND_SHOULDERS", context: "race-car" }
|
||||
```
|
||||
|
||||
### RPG/Adventure
|
||||
```javascript
|
||||
music: { mood: "Heroic", energy: 6 }
|
||||
background: { theme: "fantasy", variant: "castle" }
|
||||
avatarContext: { mode: "FULL_BODY", context: "knight-armor" }
|
||||
```
|
||||
|
||||
### Puzzle
|
||||
```javascript
|
||||
music: { mood: "Chill", energy: 3 }
|
||||
background: { theme: "abstract", variant: "minimalist" }
|
||||
// Use gameTemplateSimple.html instead
|
||||
```
|
||||
|
||||
### Horror
|
||||
```javascript
|
||||
music: { mood: "Mysterious", energy: 4 }
|
||||
background: { theme: "spooky", variant: "hauntedHouse" }
|
||||
avatarContext: { mode: "FULL_BODY", context: "adventure-hero" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Always use the provided state object** - Don't create global variables for game state.
|
||||
|
||||
2. **Delta time is in seconds** - Multiply speeds by `dt` for frame-independent movement.
|
||||
|
||||
3. **Player position is center-based** - `playerX` and `playerY` are the center of the player.
|
||||
|
||||
4. **Earned accessories always show** - User's crowns, auras, etc. render above costumes.
|
||||
|
||||
5. **Test on mobile** - Touch handlers are provided, but test gesture-based games.
|
||||
|
||||
6. **Use endGame() properly** - It handles score reporting and music fadeout.
|
||||
|
||||
7. **Don't modify the asset library scripts** - Only modify the game logic section.
|
||||
@@ -0,0 +1,905 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title><!-- GAME_TITLE --></title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
touch-action: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
canvas { display: block; }
|
||||
|
||||
/* Game UI Overlay */
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
color: white;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Pause Menu */
|
||||
#pauseMenu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0,0,0,0.9);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 200;
|
||||
}
|
||||
#pauseMenu h2 { margin-top: 0; }
|
||||
#pauseMenu button {
|
||||
display: block;
|
||||
width: 200px;
|
||||
margin: 10px auto;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
#pauseMenu button:hover { background: #45a049; }
|
||||
|
||||
/* Loading Screen */
|
||||
#loading {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: #1a1a2e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 300;
|
||||
}
|
||||
#loading .spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #333;
|
||||
border-top-color: #4CAF50;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
#loading .progress {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading Screen -->
|
||||
<div id="loading">
|
||||
<div class="spinner"></div>
|
||||
<div class="progress">Loading assets...</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Canvas -->
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
|
||||
<!-- HUD Overlay -->
|
||||
<div id="ui">
|
||||
<div id="score">Score: 0</div>
|
||||
<div id="health">Health: 100</div>
|
||||
<!-- HAIKU: Add additional UI elements here -->
|
||||
</div>
|
||||
|
||||
<!-- Pause Menu -->
|
||||
<div id="pauseMenu">
|
||||
<h2>PAUSED</h2>
|
||||
<button onclick="resumeGame()">Resume</button>
|
||||
<button onclick="toggleMusic()">Music: ON</button>
|
||||
<button onclick="toggleSfx()">Sound: ON</button>
|
||||
<button onclick="restartGame()">Restart</button>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════
|
||||
ASSET LIBRARY SCRIPTS
|
||||
These provide: Music, Backgrounds, Avatars, Contexts
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- Music System (160 procedural songs) -->
|
||||
<script src="/assets/music/moodCategories.js"></script>
|
||||
<script src="/assets/music/musicLibrary.js"></script>
|
||||
<script src="/assets/music/musicEngine.js"></script>
|
||||
|
||||
<!-- Background System (120 procedural backgrounds) -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/space.js"></script>
|
||||
<script src="/assets/backgrounds/themes/nature.js"></script>
|
||||
<script src="/assets/backgrounds/themes/urban.js"></script>
|
||||
<script src="/assets/backgrounds/themes/fantasy.js"></script>
|
||||
<script src="/assets/backgrounds/themes/abstract.js"></script>
|
||||
<script src="/assets/backgrounds/themes/retro.js"></script>
|
||||
<script src="/assets/backgrounds/themes/indoor.js"></script>
|
||||
<script src="/assets/backgrounds/themes/weather.js"></script>
|
||||
<script src="/assets/backgrounds/themes/sports.js"></script>
|
||||
<script src="/assets/backgrounds/themes/seasonal.js"></script>
|
||||
<script src="/assets/backgrounds/themes/underwater.js"></script>
|
||||
<script src="/assets/backgrounds/themes/sky.js"></script>
|
||||
<script src="/assets/backgrounds/themes/mechanical.js"></script>
|
||||
<script src="/assets/backgrounds/themes/spooky.js"></script>
|
||||
<script src="/assets/backgrounds/themes/colorful.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
||||
|
||||
<!-- Avatar System -->
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
|
||||
<!-- Context System (26 game contexts) -->
|
||||
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
||||
<script src="/assets/avatar/contexts/costumes.js"></script>
|
||||
<script src="/assets/avatar/contexts/pure.js"></script>
|
||||
<script src="/assets/avatar/contextCatalog.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<!-- Asset Manager (Unified coordination) -->
|
||||
<script src="/assets/assetCatalog.js"></script>
|
||||
<script src="/assets/compatibility.js"></script>
|
||||
<script src="/assets/presets.js"></script>
|
||||
<script src="/assets/assetManager.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME CONFIGURATION
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// HAIKU: Fill in the values below based on the game's theme and mechanics
|
||||
|
||||
const GAME_CONFIG = {
|
||||
// Game metadata
|
||||
title: "<!-- GAME_TITLE -->",
|
||||
version: "1.0.0",
|
||||
|
||||
// Asset configuration
|
||||
assets: {
|
||||
// Music settings
|
||||
// Moods: Epic, Chill, Intense, Playful, Mysterious, Heroic, Quirky, Ambient
|
||||
music: {
|
||||
mood: "<!-- MUSIC_MOOD -->", // e.g., "Epic", "Playful", "Mysterious"
|
||||
energy: 7, // 1-10 (affects tempo/intensity)
|
||||
enabled: true,
|
||||
fadeInDuration: 1000 // ms to fade in music
|
||||
},
|
||||
|
||||
// Background settings
|
||||
// Themes: space, nature, urban, fantasy, abstract, retro, indoor, weather,
|
||||
// sports, seasonal, underwater, sky, mechanical, spooky, colorful
|
||||
background: {
|
||||
theme: "<!-- BG_THEME -->", // e.g., "space", "nature", "fantasy"
|
||||
variant: "<!-- BG_VARIANT -->", // e.g., "nebula", "forest", "castle"
|
||||
animated: true, // Enable background animation
|
||||
parallax: false, // Enable parallax scrolling
|
||||
effects: [] // Additional effects: ["stars", "rain", etc]
|
||||
},
|
||||
|
||||
// Avatar context settings
|
||||
// Modes: HEAD_ONLY, HEAD_AND_SHOULDERS, UPPER_BODY, FULL_BODY
|
||||
// Contexts: spaceship-cockpit, race-car, platformer-standard, knight-armor, etc.
|
||||
avatarContext: {
|
||||
mode: "<!-- AVATAR_MODE -->", // Display mode
|
||||
context: "<!-- CONTEXT_TYPE -->", // Context ID
|
||||
showAccessories: true, // Show earned accessories
|
||||
scale: 1.0 // Avatar scale multiplier
|
||||
},
|
||||
|
||||
// Color palette (optional override)
|
||||
// Palettes: space-blue, forest-green, sunset-orange, ocean-teal, neon-pink,
|
||||
// retro-arcade, fantasy-purple, spooky-grey, sports-red, nature-brown
|
||||
colorPalette: null // null = auto-detect from theme
|
||||
},
|
||||
|
||||
// Gameplay settings
|
||||
// HAIKU: Define game-specific variables here
|
||||
gameplay: {
|
||||
// <!-- GAMEPLAY_CONFIG -->
|
||||
// Example fields (customize per game):
|
||||
// playerSpeed: 5,
|
||||
// gravity: 0.5,
|
||||
// jumpForce: 12,
|
||||
// spawnRate: 2000,
|
||||
// difficulty: 1
|
||||
},
|
||||
|
||||
// Control scheme
|
||||
controls: {
|
||||
type: "<!-- CONTROL_TYPE -->", // "keyboard", "touch", "mouse", "tilt"
|
||||
// HAIKU: Define specific bindings in input handlers below
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME STATE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Core state
|
||||
let gameState = {
|
||||
// Player
|
||||
player: null, // Avatar with context
|
||||
playerX: 0,
|
||||
playerY: 0,
|
||||
playerVelX: 0,
|
||||
playerVelY: 0,
|
||||
|
||||
// Game progress
|
||||
score: 0,
|
||||
health: 100,
|
||||
level: 1,
|
||||
|
||||
// Game status
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
isGameOver: false,
|
||||
|
||||
// Timing
|
||||
lastTime: 0,
|
||||
deltaTime: 0,
|
||||
elapsedTime: 0,
|
||||
|
||||
// Animation
|
||||
currentAnimation: 'idle',
|
||||
animationTime: 0,
|
||||
|
||||
// HAIKU: Add game-specific state variables below
|
||||
// <!-- GAME_STATE -->
|
||||
};
|
||||
|
||||
// Settings (persisted)
|
||||
let settings = {
|
||||
musicEnabled: true,
|
||||
sfxEnabled: true,
|
||||
musicVolume: 0.7,
|
||||
sfxVolume: 0.8
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Resize canvas to fill screen
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
// Update loading progress
|
||||
function setLoadingProgress(message) {
|
||||
const progress = document.querySelector('#loading .progress');
|
||||
if (progress) progress.textContent = message;
|
||||
}
|
||||
|
||||
// Main initialization
|
||||
async function init() {
|
||||
try {
|
||||
setLoadingProgress('Initializing asset systems...');
|
||||
|
||||
// Initialize asset manager
|
||||
await AssetManager.initialize();
|
||||
|
||||
setLoadingProgress('Loading music...');
|
||||
|
||||
// Setup music
|
||||
if (GAME_CONFIG.assets.music.enabled && window.MusicEngine) {
|
||||
MusicEngine.setVolume(settings.musicVolume);
|
||||
}
|
||||
|
||||
setLoadingProgress('Loading background...');
|
||||
|
||||
// Background is rendered each frame, no preload needed
|
||||
|
||||
setLoadingProgress('Loading avatar...');
|
||||
|
||||
// Load user's avatar
|
||||
const userAvatar = await loadUserAvatar();
|
||||
|
||||
// Apply game context to avatar
|
||||
if (window.ContextRenderer) {
|
||||
gameState.player = ContextRenderer.apply(
|
||||
userAvatar,
|
||||
GAME_CONFIG.assets.avatarContext.context
|
||||
);
|
||||
} else {
|
||||
// Fallback: use avatar directly
|
||||
gameState.player = { avatar: userAvatar, context: null };
|
||||
}
|
||||
|
||||
// Position player
|
||||
gameState.playerX = canvas.width / 2;
|
||||
gameState.playerY = canvas.height / 2;
|
||||
|
||||
setLoadingProgress('Starting game...');
|
||||
|
||||
// Initialize game-specific logic
|
||||
initGame();
|
||||
|
||||
// Hide loading screen
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
|
||||
// Start music
|
||||
if (GAME_CONFIG.assets.music.enabled && window.MusicEngine) {
|
||||
MusicEngine.playRandomSong(GAME_CONFIG.assets.music.mood);
|
||||
}
|
||||
|
||||
// Start game loop
|
||||
gameState.isRunning = true;
|
||||
gameState.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Game initialization failed:', error);
|
||||
setLoadingProgress('Error loading game. Please refresh.');
|
||||
}
|
||||
}
|
||||
|
||||
// Load user avatar from API or use default
|
||||
async function loadUserAvatar() {
|
||||
try {
|
||||
// Try to get user profile from parent window (if in iframe)
|
||||
if (window.parent && window.parent !== window) {
|
||||
const message = await new Promise((resolve, reject) => {
|
||||
const handler = (e) => {
|
||||
if (e.data && e.data.type === 'avatarData') {
|
||||
window.removeEventListener('message', handler);
|
||||
resolve(e.data.avatar);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handler);
|
||||
window.parent.postMessage({ type: 'requestAvatar' }, '*');
|
||||
setTimeout(() => reject(new Error('Timeout')), 2000);
|
||||
});
|
||||
|
||||
if (message && window.AvatarSystem) {
|
||||
return AvatarSystem.importMinimal(message, message.userId || 'player');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall through to default
|
||||
}
|
||||
|
||||
// Return default avatar
|
||||
if (window.AvatarSystem) {
|
||||
return AvatarSystem.createDefault();
|
||||
}
|
||||
|
||||
// Minimal fallback
|
||||
return { base: { skinTone: '#FFD5B8' } };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITY FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Update score
|
||||
function updateScore(points) {
|
||||
gameState.score += points;
|
||||
document.getElementById('score').textContent = `Score: ${gameState.score}`;
|
||||
|
||||
// Notify parent window
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'scoreUpdate', score: gameState.score }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Update health
|
||||
function updateHealth(amount) {
|
||||
gameState.health = Math.max(0, Math.min(100, gameState.health + amount));
|
||||
document.getElementById('health').textContent = `Health: ${gameState.health}`;
|
||||
|
||||
// Trigger hurt animation
|
||||
if (amount < 0) {
|
||||
setPlayerAnimation('hurt');
|
||||
setTimeout(() => setPlayerAnimation('idle'), 500);
|
||||
}
|
||||
|
||||
// Check for game over
|
||||
if (gameState.health <= 0) {
|
||||
endGame();
|
||||
}
|
||||
}
|
||||
|
||||
// Set player animation
|
||||
function setPlayerAnimation(animationId) {
|
||||
if (gameState.currentAnimation !== animationId) {
|
||||
gameState.currentAnimation = animationId;
|
||||
gameState.animationTime = 0;
|
||||
|
||||
if (window.ContextRenderer && gameState.player) {
|
||||
ContextRenderer.animate(gameState.player, animationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Play sound effect
|
||||
function playSfx(name) {
|
||||
if (!settings.sfxEnabled) return;
|
||||
// HAIKU: Implement sound effects if needed
|
||||
// Example: new Audio(`/sounds/${name}.mp3`).play();
|
||||
}
|
||||
|
||||
// Random number helpers
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function randomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
// Collision detection
|
||||
function rectCollision(r1, r2) {
|
||||
return r1.x < r2.x + r2.width &&
|
||||
r1.x + r1.width > r2.x &&
|
||||
r1.y < r2.y + r2.height &&
|
||||
r1.y + r1.height > r2.y;
|
||||
}
|
||||
|
||||
function circleCollision(c1, c2) {
|
||||
const dx = c1.x - c2.x;
|
||||
const dy = c1.y - c2.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
return dist < c1.radius + c2.radius;
|
||||
}
|
||||
|
||||
// Clamp value
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
// Lerp (linear interpolation)
|
||||
function lerp(a, b, t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LIFECYCLE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function pauseGame() {
|
||||
if (gameState.isGameOver) return;
|
||||
gameState.isPaused = true;
|
||||
document.getElementById('pauseMenu').style.display = 'block';
|
||||
|
||||
if (window.MusicEngine) {
|
||||
MusicEngine.setVolume(settings.musicVolume * 0.3);
|
||||
}
|
||||
|
||||
// Notify parent
|
||||
if (window.pauseGame) window.pauseGame();
|
||||
}
|
||||
|
||||
function resumeGame() {
|
||||
gameState.isPaused = false;
|
||||
document.getElementById('pauseMenu').style.display = 'none';
|
||||
gameState.lastTime = performance.now();
|
||||
|
||||
if (window.MusicEngine) {
|
||||
MusicEngine.setVolume(settings.musicVolume);
|
||||
}
|
||||
|
||||
// Notify parent
|
||||
if (window.resumeGame) window.resumeGame();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
function toggleMusic() {
|
||||
settings.musicEnabled = !settings.musicEnabled;
|
||||
const btn = document.querySelector('#pauseMenu button:nth-child(2)');
|
||||
btn.textContent = `Music: ${settings.musicEnabled ? 'ON' : 'OFF'}`;
|
||||
|
||||
if (window.MusicEngine) {
|
||||
if (settings.musicEnabled) {
|
||||
MusicEngine.setVolume(settings.musicVolume);
|
||||
} else {
|
||||
MusicEngine.setVolume(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSfx() {
|
||||
settings.sfxEnabled = !settings.sfxEnabled;
|
||||
const btn = document.querySelector('#pauseMenu button:nth-child(3)');
|
||||
btn.textContent = `Sound: ${settings.sfxEnabled ? 'ON' : 'OFF'}`;
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
// Reset state
|
||||
gameState.score = 0;
|
||||
gameState.health = 100;
|
||||
gameState.level = 1;
|
||||
gameState.isGameOver = false;
|
||||
gameState.playerX = canvas.width / 2;
|
||||
gameState.playerY = canvas.height / 2;
|
||||
gameState.playerVelX = 0;
|
||||
gameState.playerVelY = 0;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('score').textContent = 'Score: 0';
|
||||
document.getElementById('health').textContent = 'Health: 100';
|
||||
|
||||
// Resume
|
||||
resumeGame();
|
||||
|
||||
// Reinitialize game-specific state
|
||||
initGame();
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
gameState.isGameOver = true;
|
||||
gameState.isRunning = false;
|
||||
|
||||
// Fade out music
|
||||
if (window.MusicEngine) {
|
||||
MusicEngine.fadeOut(2000);
|
||||
}
|
||||
|
||||
// Report final score to parent
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'gameOver',
|
||||
score: gameState.score,
|
||||
screenshot: captureScreenshot()
|
||||
}, '*');
|
||||
}
|
||||
|
||||
// Show game over (parent handles this, but fallback)
|
||||
setTimeout(() => {
|
||||
if (confirm(`Game Over!\n\nFinal Score: ${gameState.score}\n\nPlay again?`)) {
|
||||
restartGame();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function captureScreenshot() {
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
// Expose functions for parent window control
|
||||
window.pauseGame = pauseGame;
|
||||
window.resumeGame = resumeGame;
|
||||
window.pauseMusic = () => { if (window.MusicEngine) MusicEngine.setVolume(0); };
|
||||
window.resumeMusic = () => { if (window.MusicEngine) MusicEngine.setVolume(settings.musicVolume); };
|
||||
window.setMusicEnabled = (enabled) => { settings.musicEnabled = enabled; toggleMusic(); toggleMusic(); };
|
||||
window.setSfxEnabled = (enabled) => { settings.sfxEnabled = enabled; };
|
||||
window.gameScore = () => gameState.score;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RENDERING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function renderBackground(time) {
|
||||
const bg = GAME_CONFIG.assets.background;
|
||||
|
||||
if (window.BackgroundEngine) {
|
||||
BackgroundEngine.render(
|
||||
ctx,
|
||||
bg.theme,
|
||||
bg.variant,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
time / 1000,
|
||||
{ effects: bg.effects }
|
||||
);
|
||||
} else {
|
||||
// Fallback: solid color
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlayer() {
|
||||
const config = GAME_CONFIG.assets.avatarContext;
|
||||
const scale = config.scale || 1;
|
||||
|
||||
if (window.ContextRenderer && gameState.player) {
|
||||
ContextRenderer.render(
|
||||
gameState.player,
|
||||
ctx,
|
||||
gameState.playerX,
|
||||
gameState.playerY,
|
||||
{
|
||||
scale: scale,
|
||||
time: gameState.elapsedTime / 1000,
|
||||
animation: gameState.currentAnimation,
|
||||
frame: Math.floor(gameState.animationTime / 100) // ~10 fps animation
|
||||
}
|
||||
);
|
||||
} else if (window.AvatarRenderer && gameState.player) {
|
||||
// Fallback: render avatar directly
|
||||
AvatarRenderer.render(
|
||||
ctx,
|
||||
gameState.player.avatar || gameState.player,
|
||||
config.mode,
|
||||
gameState.playerX,
|
||||
gameState.playerY,
|
||||
{ scale: scale }
|
||||
);
|
||||
} else {
|
||||
// Minimal fallback: draw circle
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
ctx.beginPath();
|
||||
ctx.arc(gameState.playerX, gameState.playerY, 30 * scale, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOOP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function gameLoop(currentTime) {
|
||||
if (!gameState.isRunning || gameState.isPaused) return;
|
||||
|
||||
// Calculate delta time
|
||||
gameState.deltaTime = currentTime - gameState.lastTime;
|
||||
gameState.lastTime = currentTime;
|
||||
gameState.elapsedTime += gameState.deltaTime;
|
||||
gameState.animationTime += gameState.deltaTime;
|
||||
|
||||
// Clear canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 1. Render background
|
||||
renderBackground(gameState.elapsedTime);
|
||||
|
||||
// 2. Update game logic
|
||||
// HAIKU: Call your update function here
|
||||
updateGame(gameState.deltaTime / 1000); // Pass delta in seconds
|
||||
|
||||
// 3. Render game objects
|
||||
// HAIKU: Call your render function here
|
||||
renderGame();
|
||||
|
||||
// 4. Render player
|
||||
renderPlayer();
|
||||
|
||||
// 5. Render UI overlay (if any canvas-based UI)
|
||||
// HAIKU: Render any canvas-based UI here
|
||||
renderUI();
|
||||
|
||||
// Continue loop
|
||||
if (!gameState.isGameOver) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INPUT HANDLING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Track pressed keys
|
||||
const keys = {};
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
keys[e.code] = true;
|
||||
|
||||
// Pause on Escape
|
||||
if (e.code === 'Escape') {
|
||||
if (gameState.isPaused) resumeGame();
|
||||
else pauseGame();
|
||||
return;
|
||||
}
|
||||
|
||||
// HAIKU: Handle game-specific key presses
|
||||
handleKeyDown(e.code);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keys[e.code] = false;
|
||||
|
||||
// HAIKU: Handle game-specific key releases
|
||||
handleKeyUp(e.code);
|
||||
});
|
||||
|
||||
// Touch/mouse input
|
||||
let touchStartX = 0, touchStartY = 0;
|
||||
let isTouching = false;
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
touchStartX = e.clientX;
|
||||
touchStartY = e.clientY;
|
||||
isTouching = true;
|
||||
handleTouchStart(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (isTouching) {
|
||||
handleTouchMove(e.clientX, e.clientY, e.clientX - touchStartX, e.clientY - touchStartY);
|
||||
}
|
||||
handleMouseMove(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', (e) => {
|
||||
isTouching = false;
|
||||
handleTouchEnd(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
touchStartX = touch.clientX;
|
||||
touchStartY = touch.clientY;
|
||||
isTouching = true;
|
||||
handleTouchStart(touch.clientX, touch.clientY);
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
handleTouchMove(touch.clientX, touch.clientY, touch.clientX - touchStartX, touch.clientY - touchStartY);
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', (e) => {
|
||||
isTouching = false;
|
||||
handleTouchEnd(touchStartX, touchStartY);
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME-SPECIFIC CODE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// HAIKU: Implement the functions below with your game logic
|
||||
|
||||
/**
|
||||
* Initialize game-specific state
|
||||
* Called once at start and on restart
|
||||
*/
|
||||
function initGame() {
|
||||
// HAIKU: Initialize your game objects, enemies, items, etc.
|
||||
// Example:
|
||||
// gameState.enemies = [];
|
||||
// gameState.items = [];
|
||||
// spawnInitialObjects();
|
||||
|
||||
setPlayerAnimation('idle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game logic
|
||||
* @param {number} dt - Delta time in seconds
|
||||
*/
|
||||
function updateGame(dt) {
|
||||
// HAIKU: Update your game objects here
|
||||
// Example:
|
||||
// updateEnemies(dt);
|
||||
// updateItems(dt);
|
||||
// checkCollisions();
|
||||
// spawnNewObjects();
|
||||
|
||||
// Example: Continuous key input for movement
|
||||
// if (keys['ArrowLeft'] || keys['KeyA']) gameState.playerX -= 300 * dt;
|
||||
// if (keys['ArrowRight'] || keys['KeyD']) gameState.playerX += 300 * dt;
|
||||
// if (keys['ArrowUp'] || keys['KeyW']) gameState.playerY -= 300 * dt;
|
||||
// if (keys['ArrowDown'] || keys['KeyS']) gameState.playerY += 300 * dt;
|
||||
|
||||
// Keep player in bounds
|
||||
// gameState.playerX = clamp(gameState.playerX, 50, canvas.width - 50);
|
||||
// gameState.playerY = clamp(gameState.playerY, 50, canvas.height - 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render game objects (not player, not background)
|
||||
*/
|
||||
function renderGame() {
|
||||
// HAIKU: Render your game objects here
|
||||
// Example:
|
||||
// gameState.enemies.forEach(enemy => renderEnemy(enemy));
|
||||
// gameState.items.forEach(item => renderItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render any canvas-based UI (health bars, indicators, etc.)
|
||||
*/
|
||||
function renderUI() {
|
||||
// HAIKU: Render canvas-based UI elements
|
||||
// Example health bar:
|
||||
// const barWidth = 200;
|
||||
// const healthPct = gameState.health / 100;
|
||||
// ctx.fillStyle = '#333';
|
||||
// ctx.fillRect(canvas.width - barWidth - 20, 20, barWidth, 20);
|
||||
// ctx.fillStyle = healthPct > 0.3 ? '#4CAF50' : '#f44336';
|
||||
// ctx.fillRect(canvas.width - barWidth - 20, 20, barWidth * healthPct, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key down event
|
||||
* @param {string} code - Key code (e.g., 'Space', 'ArrowUp')
|
||||
*/
|
||||
function handleKeyDown(code) {
|
||||
// HAIKU: Handle instant key actions (jump, shoot, etc.)
|
||||
// Example:
|
||||
// if (code === 'Space') jump();
|
||||
// if (code === 'KeyX') shoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key up event
|
||||
* @param {string} code - Key code
|
||||
*/
|
||||
function handleKeyUp(code) {
|
||||
// HAIKU: Handle key release actions
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch/click start
|
||||
* @param {number} x - X position
|
||||
* @param {number} y - Y position
|
||||
*/
|
||||
function handleTouchStart(x, y) {
|
||||
// HAIKU: Handle touch/click start
|
||||
// Example: Check if clicked on a button, start drag, etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch/mouse move
|
||||
* @param {number} x - Current X
|
||||
* @param {number} y - Current Y
|
||||
* @param {number} dx - Delta X from start
|
||||
* @param {number} dy - Delta Y from start
|
||||
*/
|
||||
function handleTouchMove(x, y, dx, dy) {
|
||||
// HAIKU: Handle drag/swipe
|
||||
// Example: gameState.playerX = x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch/click end
|
||||
* @param {number} x - X position
|
||||
* @param {number} y - Y position
|
||||
*/
|
||||
function handleTouchEnd(x, y) {
|
||||
// HAIKU: Handle touch/click end
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse move (hover, not dragging)
|
||||
* @param {number} x - X position
|
||||
* @param {number} y - Y position
|
||||
*/
|
||||
function handleMouseMove(x, y) {
|
||||
// HAIKU: Handle mouse hover
|
||||
// Example: Aim at cursor
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// START GAME
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Wait for DOM then initialize
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,392 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title><!-- GAME_TITLE --></title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #1a1a2e;
|
||||
touch-action: none;
|
||||
}
|
||||
canvas { display: block; }
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
text-shadow: 2px 2px 4px #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div id="ui">Score: <span id="score">0</span></div>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SIMPLE GAME TEMPLATE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// This is a minimal template without the asset library.
|
||||
// Use for simple games that don't need avatars, procedural music, etc.
|
||||
//
|
||||
// HAIKU: Fill in GAME_CONFIG and implement the game functions below.
|
||||
|
||||
const GAME_CONFIG = {
|
||||
title: "<!-- GAME_TITLE -->",
|
||||
|
||||
// Background color or gradient
|
||||
background: {
|
||||
type: "gradient", // "solid", "gradient", or "pattern"
|
||||
colors: ["#1a1a2e", "#16213e", "#0f3460"], // gradient colors
|
||||
// color: "#1a1a2e" // for solid
|
||||
},
|
||||
|
||||
// Player settings
|
||||
player: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
color: "#4CAF50",
|
||||
speed: 300 // pixels per second
|
||||
},
|
||||
|
||||
// Game settings
|
||||
// HAIKU: Add game-specific config here
|
||||
gameplay: {
|
||||
// <!-- GAMEPLAY_CONFIG -->
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SETUP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Resize to fill screen
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// Game state
|
||||
let state = {
|
||||
// Player position
|
||||
playerX: 0,
|
||||
playerY: 0,
|
||||
playerVelX: 0,
|
||||
playerVelY: 0,
|
||||
|
||||
// Score and status
|
||||
score: 0,
|
||||
gameOver: false,
|
||||
paused: false,
|
||||
|
||||
// Timing
|
||||
lastTime: 0,
|
||||
|
||||
// HAIKU: Add game-specific state
|
||||
// <!-- GAME_STATE -->
|
||||
};
|
||||
|
||||
// Input tracking
|
||||
const keys = {};
|
||||
let mouseX = 0, mouseY = 0;
|
||||
let mouseDown = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateScore(points) {
|
||||
state.score += points;
|
||||
document.getElementById('score').textContent = state.score;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.gameOver = true;
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (confirm(`Game Over! Score: ${state.score}\n\nPlay again?`)) {
|
||||
restart();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function restart() {
|
||||
state.score = 0;
|
||||
state.gameOver = false;
|
||||
state.playerX = canvas.width / 2;
|
||||
state.playerY = canvas.height / 2;
|
||||
document.getElementById('score').textContent = '0';
|
||||
initGame();
|
||||
}
|
||||
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function randomFloat(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function clamp(val, min, max) {
|
||||
return Math.max(min, Math.min(max, val));
|
||||
}
|
||||
|
||||
function distance(x1, y1, x2, y2) {
|
||||
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
}
|
||||
|
||||
function rectCollide(r1, r2) {
|
||||
return r1.x < r2.x + r2.w && r1.x + r1.w > r2.x &&
|
||||
r1.y < r2.y + r2.h && r1.y + r1.h > r2.y;
|
||||
}
|
||||
|
||||
function circleCollide(c1, c2) {
|
||||
return distance(c1.x, c1.y, c2.x, c2.y) < c1.r + c2.r;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RENDERING HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function drawBackground() {
|
||||
const bg = GAME_CONFIG.background;
|
||||
|
||||
if (bg.type === 'gradient' && bg.colors) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
bg.colors.forEach((color, i) => {
|
||||
gradient.addColorStop(i / (bg.colors.length - 1), color);
|
||||
});
|
||||
ctx.fillStyle = gradient;
|
||||
} else {
|
||||
ctx.fillStyle = bg.color || '#1a1a2e';
|
||||
}
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function drawPlayer() {
|
||||
const p = GAME_CONFIG.player;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.fillRect(
|
||||
state.playerX - p.width / 2,
|
||||
state.playerY - p.height / 2,
|
||||
p.width,
|
||||
p.height
|
||||
);
|
||||
}
|
||||
|
||||
function drawRect(x, y, w, h, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
|
||||
function drawCircle(x, y, r, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawText(text, x, y, color, size, align) {
|
||||
ctx.fillStyle = color || 'white';
|
||||
ctx.font = `${size || 20}px Arial`;
|
||||
ctx.textAlign = align || 'left';
|
||||
ctx.fillText(text, x, y);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INPUT HANDLING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
keys[e.code] = true;
|
||||
if (e.code === 'Escape') {
|
||||
state.paused = !state.paused;
|
||||
if (!state.paused) requestAnimationFrame(gameLoop);
|
||||
}
|
||||
onKeyDown(e.code);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
keys[e.code] = false;
|
||||
onKeyUp(e.code);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
mouseDown = true;
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
onMouseDown(mouseX, mouseY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', e => {
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
onMouseMove(mouseX, mouseY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', e => {
|
||||
mouseDown = false;
|
||||
onMouseUp(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
mouseDown = true;
|
||||
mouseX = e.touches[0].clientX;
|
||||
mouseY = e.touches[0].clientY;
|
||||
onMouseDown(mouseX, mouseY);
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
mouseX = e.touches[0].clientX;
|
||||
mouseY = e.touches[0].clientY;
|
||||
onMouseMove(mouseX, mouseY);
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
mouseDown = false;
|
||||
onMouseUp(mouseX, mouseY);
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOOP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function gameLoop(time) {
|
||||
if (state.gameOver || state.paused) return;
|
||||
|
||||
const dt = (time - state.lastTime) / 1000;
|
||||
state.lastTime = time;
|
||||
|
||||
// Update
|
||||
update(dt);
|
||||
|
||||
// Render
|
||||
drawBackground();
|
||||
render();
|
||||
drawPlayer();
|
||||
renderUI();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOGIC - HAIKU IMPLEMENTS THESE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Initialize game state
|
||||
*/
|
||||
function initGame() {
|
||||
state.playerX = canvas.width / 2;
|
||||
state.playerY = canvas.height / 2;
|
||||
|
||||
// HAIKU: Initialize your game objects
|
||||
// Example:
|
||||
// state.enemies = [];
|
||||
// state.items = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game logic each frame
|
||||
* @param {number} dt - Delta time in seconds
|
||||
*/
|
||||
function update(dt) {
|
||||
// HAIKU: Implement game update logic
|
||||
|
||||
// Example: Arrow key movement
|
||||
const speed = GAME_CONFIG.player.speed;
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) state.playerX -= speed * dt;
|
||||
if (keys['ArrowRight'] || keys['KeyD']) state.playerX += speed * dt;
|
||||
if (keys['ArrowUp'] || keys['KeyW']) state.playerY -= speed * dt;
|
||||
if (keys['ArrowDown'] || keys['KeyS']) state.playerY += speed * dt;
|
||||
|
||||
// Keep in bounds
|
||||
const hw = GAME_CONFIG.player.width / 2;
|
||||
const hh = GAME_CONFIG.player.height / 2;
|
||||
state.playerX = clamp(state.playerX, hw, canvas.width - hw);
|
||||
state.playerY = clamp(state.playerY, hh, canvas.height - hh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render game objects (not player, not background)
|
||||
*/
|
||||
function render() {
|
||||
// HAIKU: Render your game objects
|
||||
// Example:
|
||||
// state.enemies.forEach(e => drawCircle(e.x, e.y, e.r, e.color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render UI elements on canvas
|
||||
*/
|
||||
function renderUI() {
|
||||
// HAIKU: Render any additional UI
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key press
|
||||
*/
|
||||
function onKeyDown(code) {
|
||||
// HAIKU: Handle key press events
|
||||
// if (code === 'Space') jump();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key release
|
||||
*/
|
||||
function onKeyUp(code) {
|
||||
// HAIKU: Handle key release events
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse/touch down
|
||||
*/
|
||||
function onMouseDown(x, y) {
|
||||
// HAIKU: Handle click/tap
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse/touch move
|
||||
*/
|
||||
function onMouseMove(x, y) {
|
||||
// HAIKU: Handle mouse/touch movement
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse/touch up
|
||||
*/
|
||||
function onMouseUp(x, y) {
|
||||
// HAIKU: Handle click/tap release
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// START
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Expose for parent window
|
||||
window.pauseGame = () => { state.paused = true; };
|
||||
window.resumeGame = () => { state.paused = false; state.lastTime = performance.now(); requestAnimationFrame(gameLoop); };
|
||||
window.gameScore = state.score;
|
||||
|
||||
// Initialize and start
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user