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