Initial commit

This commit is contained in:
Allen
2026-04-30 02:14:25 +00:00
commit 7090e6f01d
243 changed files with 115122 additions and 0 deletions
@@ -0,0 +1,545 @@
// mechanical.js — Mechanical/industrial theme for procedural background system
// 8 variants: gears, circuits, factory, robotFactory, conveyorBelts, pipes, steamPunk, techLab
(function(engine) {
if (!engine) return;
var D = engine.Draw, H = engine.helpers;
// Helper: draw a gear shape with teeth
function drawGear(ctx, cx, cy, innerR, outerR, teeth, rotation, color) {
ctx.fillStyle = color;
ctx.beginPath();
for (var i = 0; i < teeth * 2; i++) {
var angle = rotation + (i / (teeth * 2)) * Math.PI * 2;
var r = i % 2 === 0 ? outerR : innerR;
var x = cx + Math.cos(angle) * r;
var y = cy + Math.sin(angle) * r;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
// Center hole
ctx.fillStyle = H.darken(color, 0.5);
ctx.beginPath();
ctx.arc(cx, cy, innerR * 0.35, 0, Math.PI * 2);
ctx.fill();
}
// Helper: draw a rivet
function drawRivet(ctx, x, y, r, color) {
var g = D.radialGradient(ctx, x - r * 0.3, y - r * 0.3, r, [[0, H.lighten(color, 0.4)], [0.6, color], [1, H.darken(color, 0.3)]]);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
// Helper: draw a pipe segment
function drawPipe(ctx, x1, y1, x2, y2, width, color) {
var angle = Math.atan2(y2 - y1, x2 - x1);
var perpX = Math.sin(angle) * width / 2;
var perpY = -Math.cos(angle) * width / 2;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x1 + perpX, y1 + perpY);
ctx.lineTo(x2 + perpX, y2 + perpY);
ctx.lineTo(x2 - perpX, y2 - perpY);
ctx.lineTo(x1 - perpX, y1 - perpY);
ctx.closePath();
ctx.fill();
// Highlight stripe
ctx.fillStyle = H.rgba(H.lighten(color, 0.3), 0.4);
ctx.beginPath();
ctx.moveTo(x1 + perpX * 0.6, y1 + perpY * 0.6);
ctx.lineTo(x2 + perpX * 0.6, y2 + perpY * 0.6);
ctx.lineTo(x2 + perpX * 0.2, y2 + perpY * 0.2);
ctx.lineTo(x1 + perpX * 0.2, y1 + perpY * 0.2);
ctx.closePath();
ctx.fill();
}
// Helper: draw a gauge
function drawGauge(ctx, cx, cy, r, needleAngle, color, rimColor) {
// Rim
ctx.strokeStyle = rimColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
// Face
ctx.fillStyle = H.darken(color, 0.6);
ctx.beginPath();
ctx.arc(cx, cy, r - 2, 0, Math.PI * 2);
ctx.fill();
// Tick marks
ctx.strokeStyle = H.rgba(rimColor, 0.5);
ctx.lineWidth = 1;
for (var i = 0; i < 8; i++) {
var a = (i / 8) * Math.PI * 2 - Math.PI / 2;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * (r * 0.7), cy + Math.sin(a) * (r * 0.7));
ctx.lineTo(cx + Math.cos(a) * (r * 0.9), cy + Math.sin(a) * (r * 0.9));
ctx.stroke();
}
// Needle
ctx.strokeStyle = '#CC3333';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(needleAngle) * r * 0.75, cy + Math.sin(needleAngle) * r * 0.75);
ctx.stroke();
// Center dot
D.circle(ctx, cx, cy, 3, rimColor);
}
engine.registerTheme('mechanical', {
gears: {
meta: { id: 'mechanical_gears', mood: 'focused', colors: ['#1A1A24', '#2A2A38', '#7A7A88', '#C08040'], tags: ['industrial', 'moving', 'metallic'], description: 'Interlocking gears turning against a dark steel backdrop', compatibleMusicMoods: ['ambient', 'electronic', 'tense'], recommendedFor: ['puzzle', 'physics', 'rpg'] },
colors: ['#1A1A24', '#2A2A38', '#7A7A88', '#C08040'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], c[0]], 90);
// Subtle metal texture via scanlines
Draw.scanlines(ctx, w, h, 3, H.rgba(c[2], 0.03));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(100);
// Draw interlocking gears at various sizes
var gearData = [];
for (var i = 0; i < 9; i++) {
gearData.push({
x: rng() * w, y: rng() * h,
inner: 15 + rng() * 40, outer: 30 + rng() * 55,
teeth: 8 + Math.floor(rng() * 10),
speed: (rng() - 0.5) * 0.8, shade: rng()
});
}
for (var i = 0; i < gearData.length; i++) {
var g = gearData[i];
var col = H.blendColor(c[2], c[3], g.shade * 0.5);
drawGear(ctx, g.x, g.y, g.inner, g.outer, g.teeth, t * g.speed, col);
}
// Small accent gears in corners
drawGear(ctx, w * 0.08, h * 0.08, 10, 18, 6, -t * 0.6, H.rgba(c[3], 0.6));
drawGear(ctx, w * 0.92, h * 0.92, 12, 22, 7, t * 0.5, H.rgba(c[3], 0.6));
// Rivets along edges
for (var i = 0; i < 12; i++) {
drawRivet(ctx, 15 + i * (w - 30) / 11, 12, 3, c[2]);
drawRivet(ctx, 15 + i * (w - 30) / 11, h - 12, 3, c[2]);
}
},
particles: { type: 'dust', count: 20, color: '#998877' }
},
circuits: {
meta: { id: 'mechanical_circuits', mood: 'focused', colors: ['#0A0F14', '#112222', '#00CC88', '#33FFAA'], tags: ['digital', 'tech', 'grid'], description: 'Circuit board traces with glowing data paths', compatibleMusicMoods: ['electronic', 'ambient', 'tense'], recommendedFor: ['puzzle', 'trivia', 'physics'] },
colors: ['#0A0F14', '#112222', '#00CC88', '#33FFAA'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.grid(ctx, w, h, 40, H.rgba(c[2], 0.06), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(150);
ctx.lineWidth = 2;
// Draw circuit traces
for (var i = 0; i < 20; i++) {
var sx = rng() * w, sy = rng() * h;
var segments = 3 + Math.floor(rng() * 5);
var pulse = 0.3 + Math.sin(t * 1.5 + i * 0.7) * 0.25;
ctx.strokeStyle = H.rgba(c[2], 0.15 + pulse * 0.3);
ctx.beginPath();
ctx.moveTo(sx, sy);
for (var j = 0; j < segments; j++) {
var dir = Math.floor(rng() * 2); // 0=horizontal, 1=vertical
var len = 20 + rng() * 80;
if (dir === 0) sx += (rng() > 0.5 ? 1 : -1) * len;
else sy += (rng() > 0.5 ? 1 : -1) * len;
sx = H.clamp(sx, 10, w - 10);
sy = H.clamp(sy, 10, h - 10);
ctx.lineTo(sx, sy);
}
ctx.stroke();
// Node at end
D.circle(ctx, sx, sy, 3, H.rgba(c[3], 0.4 + pulse * 0.4));
}
// Chip packages
for (var i = 0; i < 6; i++) {
var cx = rng() * w * 0.8 + w * 0.1;
var cy = rng() * h * 0.8 + h * 0.1;
var cw = 20 + rng() * 20, ch = 15 + rng() * 15;
Draw.rect(ctx, cx - cw / 2, cy - ch / 2, cw, ch, H.rgba(c[1], 0.8));
ctx.strokeStyle = H.rgba(c[2], 0.3);
ctx.lineWidth = 1;
ctx.strokeRect(cx - cw / 2, cy - ch / 2, cw, ch);
// Pins
for (var p = 0; p < 4; p++) {
Draw.rect(ctx, cx - cw / 2 - 4, cy - ch / 2 + 3 + p * (ch / 4), 4, 2, H.rgba(c[2], 0.3));
Draw.rect(ctx, cx + cw / 2, cy - ch / 2 + 3 + p * (ch / 4), 4, 2, H.rgba(c[2], 0.3));
}
}
// Data pulses traveling along traces
for (var i = 0; i < 8; i++) {
var px = (rng() * w + t * 60 * (rng() > 0.5 ? 1 : -1)) % w;
var py = Math.round(rng() * h / 40) * 40;
D.glow(ctx, px, py, 6, H.rgba(c[3], 0.5 + Math.sin(t * 3 + i) * 0.3));
}
},
particles: { type: 'sparkles', count: 25, color: '#33FFAA' }
},
factory: {
meta: { id: 'mechanical_factory', mood: 'busy', colors: ['#1A1510', '#2A2520', '#8B7355', '#DDAA44'], tags: ['industrial', 'gritty', 'warm'], description: 'Factory floor with smokestacks and warning stripes', compatibleMusicMoods: ['tense', 'electronic', 'action'], recommendedFor: ['action', 'physics', 'puzzle'] },
colors: ['#1A1510', '#2A2520', '#8B7355', '#DDAA44'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.darken(c[0], 0.3)], 90);
// Floor
Draw.rect(ctx, 0, h * 0.78, w, h * 0.22, H.darken(c[2], 0.5));
// Floor grid
Draw.grid(ctx, w, h * 0.22, 30, H.rgba(c[2], 0.08), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(200);
// Smokestacks in back
for (var i = 0; i < 3; i++) {
var sx = w * 0.2 + i * w * 0.3;
var sw = 25 + rng() * 15;
var sh = h * 0.35 + rng() * h * 0.1;
Draw.rect(ctx, sx - sw / 2, h * 0.78 - sh, sw, sh, H.darken(c[2], 0.3));
// Warning stripes
ctx.save();
ctx.beginPath();
ctx.rect(sx - sw / 2, h * 0.78 - sh, sw, 12);
ctx.clip();
for (var s = -2; s < sw / 8 + 2; s++) {
Draw.rect(ctx, sx - sw / 2 + s * 16, h * 0.78 - sh, 8, 12, c[3]);
}
ctx.restore();
// Smoke puffs
for (var j = 0; j < 3; j++) {
var smokeY = h * 0.78 - sh - 15 - j * 25;
var drift = Math.sin(t * 0.5 + i + j) * 10;
var alpha = 0.08 - j * 0.02;
ctx.fillStyle = H.rgba('#888888', alpha);
ctx.beginPath();
ctx.ellipse(sx + drift, smokeY, 15 + j * 10, 8 + j * 5, 0, 0, Math.PI * 2);
ctx.fill();
}
}
// Catwalk
Draw.rect(ctx, 0, h * 0.5, w, 4, c[2]);
ctx.strokeStyle = H.rgba(c[2], 0.4);
ctx.lineWidth = 1;
for (var i = 0; i < w; i += 15) {
ctx.beginPath();
ctx.moveTo(i, h * 0.5 + 4);
ctx.lineTo(i + 7, h * 0.5 + 18);
ctx.lineTo(i + 15, h * 0.5 + 4);
ctx.stroke();
}
Draw.rect(ctx, 0, h * 0.5 + 18, w, 2, H.rgba(c[2], 0.3));
// Warning light
var blink = Math.sin(t * 4) > 0 ? 0.8 : 0.1;
D.glow(ctx, w * 0.9, h * 0.15, 25, H.rgba('#FF4400', blink * 0.4));
D.circle(ctx, w * 0.9, h * 0.15, 6, H.rgba('#FF4400', blink));
// Rivets on floor seam
for (var i = 0; i < 16; i++) {
drawRivet(ctx, w * 0.05 + i * (w * 0.9 / 15), h * 0.78, 2.5, c[2]);
}
},
particles: { type: 'dust', count: 30, color: '#AA9977' }
},
robotFactory: {
meta: { id: 'mechanical_robotFactory', mood: 'playful', colors: ['#151520', '#222238', '#5588CC', '#FFAA33'], tags: ['robots', 'tech', 'fun'], description: 'Robot assembly line with mechanical arms and sparks', compatibleMusicMoods: ['electronic', 'energetic', 'playful'], recommendedFor: ['action', 'puzzle', 'creative'] },
colors: ['#151520', '#222238', '#5588CC', '#FFAA33'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Floor
Draw.rect(ctx, 0, h * 0.82, w, h * 0.18, H.darken(c[1], 0.3));
Draw.grid(ctx, w, h, 50, H.rgba(c[2], 0.04), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(250);
// Assembly arms from ceiling
for (var i = 0; i < 4; i++) {
var ax = w * 0.15 + i * w * 0.23;
var armAngle = Math.sin(t * 0.7 + i * 1.5) * 0.4;
var armLen = h * 0.25;
var elbowX = ax + Math.sin(armAngle) * armLen;
var elbowY = armLen * Math.cos(armAngle);
// Arm segment
ctx.strokeStyle = c[2];
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(ax, 0);
ctx.lineTo(elbowX, elbowY);
ctx.stroke();
// Joint
D.circle(ctx, elbowX, elbowY, 6, H.lighten(c[2], 0.2));
D.circle(ctx, ax, 0, 8, c[2]);
// Claw tip
var clawAngle = armAngle + Math.sin(t * 1.5 + i) * 0.3;
var tipX = elbowX + Math.sin(clawAngle) * 20;
var tipY = elbowY + Math.cos(clawAngle) * 20;
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(elbowX, elbowY);
ctx.lineTo(tipX, tipY);
ctx.stroke();
// Sparks at claw
if (Math.sin(t * 5 + i * 2.3) > 0.6) {
D.glow(ctx, tipX, tipY, 10, H.rgba(c[3], 0.6));
}
}
// Robot bodies on conveyor (silhouettes)
for (var i = 0; i < 3; i++) {
var rx = (w * 0.1 + i * w * 0.35 + t * 15) % (w + 60) - 30;
var ry = h * 0.82;
// Body
Draw.rect(ctx, rx - 12, ry - 35, 24, 25, c[2]);
// Head
Draw.rect(ctx, rx - 8, ry - 45, 16, 12, H.lighten(c[2], 0.15));
// Eyes
var eyeGlow = 0.4 + Math.sin(t * 3 + i) * 0.3;
D.circle(ctx, rx - 4, ry - 40, 2, H.rgba(c[3], eyeGlow));
D.circle(ctx, rx + 4, ry - 40, 2, H.rgba(c[3], eyeGlow));
// Legs
Draw.rect(ctx, rx - 10, ry - 10, 6, 10, H.darken(c[2], 0.2));
Draw.rect(ctx, rx + 4, ry - 10, 6, 10, H.darken(c[2], 0.2));
}
// Conveyor belt
Draw.rect(ctx, 0, h * 0.82, w, 5, H.darken(c[2], 0.4));
var beltOffset = (t * 30) % 20;
for (var i = -1; i < w / 20 + 1; i++) {
Draw.rect(ctx, i * 20 + beltOffset, h * 0.82, 2, 5, H.rgba(c[2], 0.3));
}
},
particles: { type: 'sparkles', count: 20, color: '#FFAA33' }
},
conveyorBelts: {
meta: { id: 'mechanical_conveyorBelts', mood: 'busy', colors: ['#18181F', '#28283A', '#666680', '#DD8822'], tags: ['movement', 'industrial', 'repetitive'], description: 'Layered conveyor belts moving crates and parts', compatibleMusicMoods: ['electronic', 'ambient', 'action'], recommendedFor: ['puzzle', 'action', 'physics'] },
colors: ['#18181F', '#28283A', '#666680', '#DD8822'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
Draw.scanlines(ctx, w, h, 4, H.rgba(c[2], 0.02));
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(300);
// Three conveyor belt layers
var beltYs = [h * 0.3, h * 0.55, h * 0.8];
var beltSpeeds = [25, -35, 20];
for (var b = 0; b < 3; b++) {
var by = beltYs[b];
var beltH = 8;
// Belt surface
Draw.rect(ctx, 0, by, w, beltH, H.darken(c[2], 0.3));
// Belt treads
var treadOffset = (t * beltSpeeds[b]) % 16;
for (var i = -1; i < w / 16 + 1; i++) {
Draw.rect(ctx, i * 16 + treadOffset, by, 2, beltH, H.rgba(c[2], 0.2));
}
// Support legs
for (var i = 0; i < 5; i++) {
var lx = w * 0.1 + i * w * 0.2;
Draw.rect(ctx, lx - 3, by + beltH, 6, h - by - beltH, H.darken(c[2], 0.5));
}
// Rollers at ends
D.circle(ctx, 15, by + beltH / 2, beltH / 2 + 2, c[2]);
D.circle(ctx, w - 15, by + beltH / 2, beltH / 2 + 2, c[2]);
// Items on belt
for (var i = 0; i < 4; i++) {
var itemX = (rng() * w + t * beltSpeeds[b]) % (w + 40) - 20;
var itemW = 12 + rng() * 18;
var itemH = 10 + rng() * 14;
var itemCol = rng() > 0.5 ? H.blendColor(c[2], c[3], rng() * 0.6) : H.darken(c[2], rng() * 0.3);
Draw.rect(ctx, itemX - itemW / 2, by - itemH, itemW, itemH, itemCol);
}
}
// Overhead arrows
ctx.strokeStyle = H.rgba(c[3], 0.2);
ctx.lineWidth = 1.5;
for (var i = 0; i < 6; i++) {
var ax = w * 0.1 + i * w * 0.16;
ctx.beginPath();
ctx.moveTo(ax, h * 0.12);
ctx.lineTo(ax + 10, h * 0.15);
ctx.lineTo(ax, h * 0.18);
ctx.stroke();
}
},
particles: { type: 'dust', count: 15, color: '#887766' }
},
pipes: {
meta: { id: 'mechanical_pipes', mood: 'complex', colors: ['#121218', '#1E2A1E', '#558855', '#88BB44'], tags: ['plumbing', 'industrial', 'network'], description: 'Interconnected pipe network with valves and joints', compatibleMusicMoods: ['ambient', 'tense', 'electronic'], recommendedFor: ['puzzle', 'physics', 'rpg'] },
colors: ['#121218', '#1E2A1E', '#558855', '#88BB44'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(350);
// Horizontal pipes
var pipeYs = [h * 0.15, h * 0.35, h * 0.55, h * 0.75, h * 0.9];
for (var i = 0; i < pipeYs.length; i++) {
var pw = 8 + rng() * 8;
var pipeCol = H.blendColor(c[2], c[3], rng() * 0.4);
drawPipe(ctx, 0, pipeYs[i], w, pipeYs[i], pw, pipeCol);
// Joints/flanges
for (var j = 0; j < 4; j++) {
var jx = rng() * w;
Draw.rect(ctx, jx - pw * 0.7, pipeYs[i] - pw * 0.7, pw * 1.4, pw * 1.4, H.darken(pipeCol, 0.15));
}
}
// Vertical connector pipes
for (var i = 0; i < 7; i++) {
var vx = rng() * w * 0.8 + w * 0.1;
var fromY = pipeYs[Math.floor(rng() * (pipeYs.length - 1))];
var toIdx = Math.min(Math.floor(rng() * (pipeYs.length - 1)) + 1, pipeYs.length - 1);
var toY = pipeYs[toIdx];
if (fromY === toY) toY = pipeYs[Math.min(toIdx + 1, pipeYs.length - 1)];
var vpw = 6 + rng() * 5;
drawPipe(ctx, vx, fromY, vx, toY, vpw, H.blendColor(c[2], c[3], rng() * 0.3));
}
// Valves (wheel shapes)
for (var i = 0; i < 4; i++) {
var vx = rng() * w * 0.7 + w * 0.15;
var vy = pipeYs[Math.floor(rng() * pipeYs.length)];
var vr = 8 + rng() * 6;
var spin = t * 0.3 * (rng() > 0.5 ? 1 : -1);
ctx.strokeStyle = H.rgba(c[3], 0.7);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(vx, vy, vr, 0, Math.PI * 2);
ctx.stroke();
for (var s = 0; s < 4; s++) {
var sa = spin + s * Math.PI / 2;
ctx.beginPath();
ctx.moveTo(vx, vy);
ctx.lineTo(vx + Math.cos(sa) * vr, vy + Math.sin(sa) * vr);
ctx.stroke();
}
}
// Dripping animation
for (var i = 0; i < 3; i++) {
var dx = rng() * w;
var dripY = pipeYs[Math.floor(rng() * pipeYs.length)] + 8;
var dropY = dripY + ((t * 40 + rng() * 100) % 50);
var alpha = 1 - ((t * 40 + rng() * 100) % 50) / 50;
D.circle(ctx, dx, dropY, 2, H.rgba(c[3], alpha * 0.5));
}
},
particles: { type: 'dust', count: 12, color: '#667755' }
},
steamPunk: {
meta: { id: 'mechanical_steamPunk', mood: 'adventurous', colors: ['#1A1008', '#2A1A0A', '#C08040', '#E8C060'], tags: ['brass', 'vintage', 'ornate'], description: 'Brass gears, gauges, and steam vents in warm tones', compatibleMusicMoods: ['epic', 'ambient', 'action'], recommendedFor: ['rpg', 'story', 'action'] },
colors: ['#1A1008', '#2A1A0A', '#C08040', '#E8C060'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1], H.darken(c[0], 0.2)], 135);
// Warm vignette
var g = D.radialGradient(ctx, w / 2, h / 2, Math.max(w, h) * 0.7, [[0, H.rgba(c[0], 0)], [1, H.rgba(c[0], 0.5)]]);
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(400);
// Large decorative gears
drawGear(ctx, w * 0.2, h * 0.3, 35, 55, 12, t * 0.15, H.rgba(c[2], 0.6));
drawGear(ctx, w * 0.35, h * 0.45, 20, 35, 8, -t * 0.2, H.rgba(c[3], 0.5));
drawGear(ctx, w * 0.8, h * 0.25, 40, 65, 14, t * 0.12, H.rgba(c[2], 0.55));
drawGear(ctx, w * 0.65, h * 0.7, 25, 42, 10, -t * 0.18, H.rgba(c[3], 0.5));
// Gauges
var needleWobble1 = -Math.PI / 4 + Math.sin(t * 0.8) * 0.5;
drawGauge(ctx, w * 0.5, h * 0.2, 22, needleWobble1, c[0], c[2]);
var needleWobble2 = Math.sin(t * 0.5) * 0.8;
drawGauge(ctx, w * 0.88, h * 0.6, 18, needleWobble2, c[0], c[3]);
// Decorative border rivets
for (var i = 0; i < 20; i++) {
drawRivet(ctx, w * 0.03, h * 0.05 + i * (h * 0.9 / 19), 3, c[2]);
drawRivet(ctx, w * 0.97, h * 0.05 + i * (h * 0.9 / 19), 3, c[2]);
}
for (var i = 0; i < 14; i++) {
drawRivet(ctx, w * 0.05 + i * (w * 0.9 / 13), h * 0.03, 3, c[2]);
drawRivet(ctx, w * 0.05 + i * (w * 0.9 / 13), h * 0.97, 3, c[2]);
}
// Steam vents
for (var v = 0; v < 2; v++) {
var vx = w * 0.3 + v * w * 0.4;
var vy = h * 0.85;
Draw.rect(ctx, vx - 6, vy, 12, h - vy, H.darken(c[2], 0.3));
// Steam puffs
for (var p = 0; p < 4; p++) {
var pAlpha = 0.06 - p * 0.012;
var drift = Math.sin(t + v + p * 0.5) * 8;
ctx.fillStyle = H.rgba('#CCBBAA', pAlpha);
ctx.beginPath();
ctx.ellipse(vx + drift, vy - 10 - p * 18, 12 + p * 8, 6 + p * 4, 0, 0, Math.PI * 2);
ctx.fill();
}
}
},
particles: { type: 'dust', count: 18, color: '#C0A060' }
},
techLab: {
meta: { id: 'mechanical_techLab', mood: 'curious', colors: ['#0E0E1A', '#1A1A30', '#4455AA', '#66DDFF'], tags: ['science', 'clean', 'futuristic'], description: 'Clean tech laboratory with holographic displays and instruments', compatibleMusicMoods: ['ambient', 'electronic', 'calm'], recommendedFor: ['puzzle', 'trivia', 'creative'] },
colors: ['#0E0E1A', '#1A1A30', '#4455AA', '#66DDFF'],
renderBase: function(ctx, w, h, c, Draw, t) {
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
// Clean floor
Draw.rect(ctx, 0, h * 0.85, w, h * 0.15, H.darken(c[1], 0.2));
Draw.grid(ctx, w, h, 60, H.rgba(c[2], 0.04), 0.5);
},
renderMid: function(ctx, w, h, c, Draw, t) {
var rng = H.seededRandom(450);
// Holographic display panels
for (var i = 0; i < 3; i++) {
var px = w * 0.12 + i * w * 0.32;
var py = h * 0.15;
var pw = w * 0.22, ph = h * 0.35;
// Panel frame
ctx.strokeStyle = H.rgba(c[2], 0.4);
ctx.lineWidth = 1.5;
ctx.strokeRect(px, py, pw, ph);
// Panel glow fill
ctx.fillStyle = H.rgba(c[2], 0.05 + Math.sin(t + i) * 0.02);
ctx.fillRect(px, py, pw, ph);
// Data lines inside panel
ctx.strokeStyle = H.rgba(c[3], 0.2);
ctx.lineWidth = 1;
for (var l = 0; l < 5; l++) {
var ly = py + 10 + l * (ph - 20) / 5;
ctx.beginPath();
ctx.moveTo(px + 8, ly);
for (var x = 0; x < pw - 16; x += 4) {
var val = Math.sin(x * 0.1 + t * 2 + l * 1.3 + i) * 6;
ctx.lineTo(px + 8 + x, ly + val);
}
ctx.stroke();
}
// Blinking status light
var statusBlink = Math.sin(t * 2.5 + i * 1.1) > 0 ? 0.8 : 0.2;
D.circle(ctx, px + pw - 8, py + 8, 3, H.rgba(c[3], statusBlink));
}
// Lab equipment silhouettes
for (var i = 0; i < 4; i++) {
var ex = rng() * w * 0.8 + w * 0.1;
var eH = 15 + rng() * 30;
Draw.rect(ctx, ex - 8, h * 0.85 - eH, 16, eH, H.rgba(c[2], 0.3));
D.circle(ctx, ex, h * 0.85 - eH - 5, 4, H.rgba(c[3], 0.3));
}
// Floating data points
for (var i = 0; i < 10; i++) {
var dx = rng() * w;
var dy = rng() * h * 0.8;
var flicker = 0.15 + Math.sin(t * 1.8 + rng() * 10) * 0.15;
D.circle(ctx, dx, dy, 1.5, H.rgba(c[3], flicker));
}
},
particles: { type: 'sparkles', count: 25, color: '#66DDFF' }
}
});
})(window.BackgroundEngine);