Initial commit
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user