// 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);