Files
2026-04-30 02:14:25 +00:00

695 lines
28 KiB
JavaScript

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