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