695 lines
28 KiB
JavaScript
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);
|