// retro.js — Retro theme backgrounds // Variants: pixelGrid, arcadeCabinet, crtEffects, vaporwave, scanlines, glitch, eightBit, synthwave (function(engine) { if (!engine) return; var D = engine.Draw, H = engine.helpers; engine.registerTheme('retro', { pixelGrid: { meta: { id: 'retro_pixelGrid', mood: 'nostalgic', colors: ['#1a1a2e', '#16213e', '#00ff88', '#ff6600'], tags: ['pixel', 'grid', 'retro', 'classic'], description: 'A chunky pixel grid with classic 8-bit color blocks and pulsing cursor', compatibleMusicMoods: ['retro', 'playful', 'electronic'], recommendedFor: ['action', 'puzzle', 'creative'] }, colors: ['#1a1a2e', '#16213e', '#00ff88', '#ff6600'], renderBase: function(ctx, w, h, c, Draw, t) { Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90); Draw.grid(ctx, w, h, 32, H.rgba(c[2], 0.04), 1); }, renderMid: function(ctx, w, h, c, Draw, t) { var rng = H.seededRandom(100); var pixelSize = 32; var cols = Math.ceil(w / pixelSize); var rows = Math.ceil(h / pixelSize); // Scattered colored pixels for (var i = 0; i < 30; i++) { var px = Math.floor(rng() * cols) * pixelSize; var py = Math.floor(rng() * rows) * pixelSize; var pColor = rng() > 0.5 ? c[2] : c[3]; var pAlpha = 0.06 + rng() * 0.1 + Math.sin(t * 0.8 + i * 0.5) * 0.03; Draw.rect(ctx, px, py, pixelSize - 1, pixelSize - 1, H.rgba(pColor, pAlpha)); } // Pixel art sprite (simple ghost/invader shape) var spriteX = w * 0.5, spriteY = h * 0.4; var sp = pixelSize * 0.5; var spriteMap = [ [0,0,1,1,1,0,0], [0,1,1,1,1,1,0], [1,1,0,1,0,1,1], [1,1,1,1,1,1,1], [1,0,1,0,1,0,1], [0,1,0,1,0,1,0] ]; var spriteAlpha = 0.15 + Math.sin(t * 0.6) * 0.05; var sOffX = spriteX - (spriteMap[0].length * sp) / 2; var sOffY = spriteY - (spriteMap.length * sp) / 2; for (var sr = 0; sr < spriteMap.length; sr++) { for (var sc = 0; sc < spriteMap[sr].length; sc++) { if (spriteMap[sr][sc]) { Draw.rect(ctx, sOffX + sc * sp, sOffY + sr * sp, sp - 1, sp - 1, H.rgba(c[2], spriteAlpha)); } } } // Blinking cursor var cursorOn = Math.sin(t * 3) > 0; if (cursorOn) { Draw.rect(ctx, w * 0.3, h * 0.75, pixelSize * 0.6, pixelSize, H.rgba(c[2], 0.4)); } // Score text (pixel-styled line) for (var sc2 = 0; sc2 < 6; sc2++) { Draw.rect(ctx, w * 0.35 + sc2 * 12, h * 0.12, 8, 3, H.rgba(c[3], 0.12)); } }, particles: { type: 'sparkles', count: 10, color: '#00FF88' } }, arcadeCabinet: { meta: { id: 'retro_arcadeCabinet', mood: 'fun', colors: ['#0a0a15', '#1a0a2e', '#ff2277', '#ffcc00'], tags: ['arcade', 'cabinet', 'neon', 'classic'], description: 'The inside view of a dark arcade with neon-lit cabinet edges and attract mode glow', compatibleMusicMoods: ['retro', 'energetic', 'playful'], recommendedFor: ['action', 'racing', 'sports'] }, colors: ['#0a0a15', '#1a0a2e', '#ff2277', '#ffcc00'], renderBase: function(ctx, w, h, c, Draw, t) { Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90); // Ambient arcade glow Draw.glow(ctx, w * 0.5, h * 0.5, w * 0.5, H.rgba(c[2], 0.04)); }, renderMid: function(ctx, w, h, c, Draw, t) { var rng = H.seededRandom(200); // Floor tiles (perspective grid) ctx.strokeStyle = H.rgba(c[2], 0.05); ctx.lineWidth = 1; for (var fl = 0; fl < 15; fl++) { var fy = h * 0.7 + fl * h * 0.02; ctx.beginPath(); ctx.moveTo(0, fy); ctx.lineTo(w, fy); ctx.stroke(); } for (var fv = 0; fv < 12; fv++) { var fx = fv * w / 12; ctx.beginPath(); ctx.moveTo(fx, h * 0.7); ctx.lineTo(w * 0.5 + (fx - w * 0.5) * 0.3, h); ctx.stroke(); } // Arcade cabinets (silhouettes with neon trim) var cabinets = [ { x: 0.12, scale: 0.9 }, { x: 0.32, scale: 1.0 }, { x: 0.52, scale: 1.0 }, { x: 0.72, scale: 0.9 }, { x: 0.88, scale: 0.8 } ]; for (var cab = 0; cab < cabinets.length; cab++) { var cbx = cabinets[cab].x * w; var cbs = cabinets[cab].scale; var cbw = w * 0.12 * cbs; var cbh = h * 0.45 * cbs; var cby = h * 0.68 - cbh; // Cabinet body Draw.rect(ctx, cbx - cbw / 2, cby, cbw, cbh, '#0a0a12'); // Screen area var scrW = cbw * 0.7, scrH = cbh * 0.4; var scrX = cbx - scrW / 2, scrY = cby + cbh * 0.1; var screenColor = rng() > 0.5 ? c[2] : c[3]; var screenGlow = 0.15 + Math.sin(t * 1.5 + cab * 1.2) * 0.08; Draw.rect(ctx, scrX, scrY, scrW, scrH, H.rgba(screenColor, screenGlow)); Draw.glow(ctx, cbx, scrY + scrH / 2, scrW * 0.6, H.rgba(screenColor, screenGlow * 0.3)); // Neon trim ctx.strokeStyle = H.rgba(screenColor, 0.25 + Math.sin(t * 2 + cab) * 0.1); ctx.lineWidth = 1.5; ctx.strokeRect(scrX - 2, scrY - 2, scrW + 4, scrH + 4); // Control panel Draw.rect(ctx, cbx - cbw * 0.4, cby + cbh * 0.7, cbw * 0.8, cbh * 0.1, '#151520'); // Joystick and buttons Draw.circle(ctx, cbx - cbw * 0.15, cby + cbh * 0.75, 3, '#333'); for (var btn = 0; btn < 3; btn++) { Draw.circle(ctx, cbx + cbw * 0.05 + btn * 10, cby + cbh * 0.75, 3, H.rgba(btn === 0 ? '#FF2277' : btn === 1 ? '#2277FF' : '#FFCC00', 0.4)); } } // "INSERT COIN" text glow var coinBlink = Math.sin(t * 2) > 0; if (coinBlink) { ctx.font = 'bold 14px monospace'; ctx.fillStyle = H.rgba(c[3], 0.3); ctx.textAlign = 'center'; ctx.fillText('INSERT COIN', w * 0.5, h * 0.88); ctx.textAlign = 'start'; } // Neon sign at top ctx.strokeStyle = H.rgba(c[2], 0.2 + Math.sin(t * 1.8) * 0.08); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(w * 0.25, h * 0.08); ctx.lineTo(w * 0.75, h * 0.08); ctx.stroke(); Draw.glow(ctx, w * 0.5, h * 0.08, w * 0.3, H.rgba(c[2], 0.04)); }, particles: { type: 'dust', count: 15, color: '#FF2277' } }, crtEffects: { meta: { id: 'retro_crtEffects', mood: 'vintage', colors: ['#0a0a0a', '#0f1a0f', '#33ff33', '#88ff88'], tags: ['crt', 'monitor', 'phosphor', 'vintage'], description: 'Authentic CRT monitor with phosphor glow, barrel distortion and scan line interference', compatibleMusicMoods: ['retro', 'dark', 'electronic'], recommendedFor: ['action', 'puzzle', 'trivia'] }, colors: ['#0a0a0a', '#0f1a0f', '#33ff33', '#88ff88'], renderBase: function(ctx, w, h, c, Draw, t) { Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90); // Phosphor glow in center Draw.glow(ctx, w * 0.5, h * 0.5, Math.min(w, h) * 0.5, H.rgba(c[2], 0.03)); }, renderMid: function(ctx, w, h, c, Draw, t) { // Scan lines Draw.scanlines(ctx, w, h, 3, H.rgba(c[2], 0.03 + Math.sin(t * 0.5) * 0.01)); // RGB sub-pixel columns for (var col = 0; col < w; col += 3) { Draw.rect(ctx, col, 0, 1, h, H.rgba('#FF0000', 0.008)); Draw.rect(ctx, col + 1, 0, 1, h, H.rgba('#00FF00', 0.008)); Draw.rect(ctx, col + 2, 0, 1, h, H.rgba('#0000FF', 0.008)); } // Rolling interference band var bandY = ((t * 30) % (h + 80)) - 40; ctx.fillStyle = H.rgba(c[2], 0.04); ctx.fillRect(0, bandY, w, 40); ctx.fillStyle = H.rgba(c[2], 0.06); ctx.fillRect(0, bandY + 15, w, 8); // Static noise bursts var rng = H.seededRandom(Math.floor(t * 5) + 300); if (rng() > 0.7) { for (var nz = 0; nz < 50; nz++) { var nx = rng() * w, ny = rng() * h; var nw = 1 + rng() * 4; Draw.rect(ctx, nx, ny, nw, 1, H.rgba(c[2], rng() * 0.15)); } } // Faux text content var txtrng = H.seededRandom(301); ctx.font = '12px monospace'; for (var ln = 0; ln < 8; ln++) { var lineY = h * 0.25 + ln * 22; var lineStr = ''; var lineLen = 10 + Math.floor(txtrng() * 25); for (var ch = 0; ch < lineLen; ch++) { lineStr += String.fromCharCode(33 + Math.floor(txtrng() * 93)); } var txtAlpha = 0.08 + Math.sin(t * 0.4 + ln * 0.5) * 0.03; ctx.fillStyle = H.rgba(c[2], txtAlpha); ctx.fillText(lineStr, w * 0.15, lineY); } // Prompt cursor var cursorBlink = Math.sin(t * 3) > 0; if (cursorBlink) { Draw.rect(ctx, w * 0.15, h * 0.25 + 8 * 22, 8, 14, H.rgba(c[2], 0.3)); } // Vignette / barrel distortion feel var vig = Draw.radialGradient(ctx, w / 2, h / 2, Math.max(w, h) * 0.65, [ [0, H.rgba(c[0], 0)], [0.6, H.rgba(c[0], 0)], [1, H.rgba(c[0], 0.7)] ]); ctx.fillStyle = vig; ctx.fillRect(0, 0, w, h); // CRT bezel border ctx.strokeStyle = H.rgba('#333333', 0.3); ctx.lineWidth = 8; ctx.strokeRect(4, 4, w - 8, h - 8); }, particles: null }, vaporwave: { meta: { id: 'retro_vaporwave', mood: 'aesthetic', colors: ['#0d0221', '#261447', '#ff71ce', '#01cdfe'], tags: ['vaporwave', 'aesthetic', '90s', 'neon'], description: 'A e s t h e t i c sunset horizon with a perspective grid, palm silhouettes and neon glow', compatibleMusicMoods: ['dreamy', 'retro', 'calm'], recommendedFor: ['creative', 'music', 'racing'] }, colors: ['#0d0221', '#261447', '#ff71ce', '#01cdfe'], renderBase: function(ctx, w, h, c, Draw, t) { // Sunset gradient Draw.fillGradient(ctx, w, h, [c[0], c[1], '#ff71ce', '#ff9de2'], 90); // Sun var sunY = h * 0.42 + Math.sin(t * 0.2) * 3; var sunGrad = Draw.linearGradient(ctx, 0, sunY - 60, 0, sunY + 60, [ [0, '#FF6B9D'], [0.5, '#FFBE0B'], [1, '#FF006E'] ]); ctx.fillStyle = sunGrad; ctx.beginPath(); ctx.arc(w * 0.5, sunY, 60, 0, Math.PI * 2); ctx.fill(); // Sun horizontal stripes for (var ss = 0; ss < 6; ss++) { var ssy = sunY + ss * 10 - 10; var ssH = 3 + ss * 0.8; Draw.rect(ctx, w * 0.5 - 60, ssy, 120, ssH, c[0]); } Draw.glow(ctx, w * 0.5, sunY, 120, H.rgba('#FF71CE', 0.15)); }, renderMid: function(ctx, w, h, c, Draw, t) { // Horizon line var horizonY = h * 0.55; Draw.rect(ctx, 0, horizonY, w, h - horizonY, '#0d0221'); // Perspective grid on ground plane var gridColor = H.rgba(c[2], 0.2); ctx.strokeStyle = gridColor; ctx.lineWidth = 1; // Horizontal lines (receding) for (var gy = 0; gy < 20; gy++) { var lineFrac = gy / 20; var lineY = horizonY + lineFrac * lineFrac * (h - horizonY); ctx.beginPath(); ctx.moveTo(0, lineY); ctx.lineTo(w, lineY); ctx.stroke(); } // Vertical lines (converging at center horizon) for (var gx = -8; gx <= 8; gx++) { var topX = w * 0.5 + gx * 2; var botX = w * 0.5 + gx * w * 0.08; ctx.strokeStyle = H.rgba(c[3], 0.15); ctx.beginPath(); ctx.moveTo(topX, horizonY); ctx.lineTo(botX, h); ctx.stroke(); } // Palm tree silhouettes var rng = H.seededRandom(400); var palms = [{ x: 0.12, s: 1.0 }, { x: 0.85, s: 0.9 }, { x: 0.05, s: 0.7 }]; for (var p = 0; p < palms.length; p++) { var px = palms[p].x * w; var ps = palms[p].s; var trunkH = h * 0.35 * ps; var palmBot = horizonY; // Trunk ctx.strokeStyle = '#0d0221'; ctx.lineWidth = 4 * ps; ctx.beginPath(); ctx.moveTo(px, palmBot); ctx.quadraticCurveTo(px + 10 * ps, palmBot - trunkH * 0.6, px + 5 * ps, palmBot - trunkH); ctx.stroke(); // Fronds for (var fr = 0; fr < 6; fr++) { var frAngle = (fr / 6) * Math.PI * 2; var frLen = 30 * ps + Math.sin(t * 0.8 + fr) * 3; var sway = Math.sin(t * 0.5 + fr * 0.3) * 5; ctx.strokeStyle = '#0d0221'; ctx.lineWidth = 2 * ps; ctx.beginPath(); ctx.moveTo(px + 5 * ps, palmBot - trunkH); ctx.quadraticCurveTo( px + 5 * ps + Math.cos(frAngle) * frLen * 0.5 + sway, palmBot - trunkH + Math.sin(frAngle) * frLen * 0.3 - 10, px + 5 * ps + Math.cos(frAngle) * frLen + sway, palmBot - trunkH + Math.sin(frAngle) * frLen * 0.5 + 5 ); ctx.stroke(); } } // Neon highlights on horizon ctx.strokeStyle = H.rgba(c[3], 0.3 + Math.sin(t) * 0.1); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, horizonY); ctx.lineTo(w, horizonY); ctx.stroke(); }, particles: { type: 'stars', count: 40, color: '#FF71CE' } }, scanlines: { meta: { id: 'retro_scanlines', mood: 'technical', colors: ['#0a0a1a', '#141428', '#00aaff', '#ff4444'], tags: ['scanlines', 'display', 'technical', 'monitor'], description: 'Dense horizontal scan lines over a shifting color field with signal noise artifacts', compatibleMusicMoods: ['electronic', 'tense', 'dark'], recommendedFor: ['action', 'puzzle', 'physics'] }, colors: ['#0a0a1a', '#141428', '#00aaff', '#ff4444'], 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) { // Color field blocks shifting var rng = H.seededRandom(500); for (var blk = 0; blk < 8; blk++) { var bx = rng() * w * 0.8; var by = rng() * h * 0.8; var bw = w * 0.1 + rng() * w * 0.2; var bh = h * 0.05 + rng() * h * 0.15; var bColor = rng() > 0.5 ? c[2] : c[3]; var bAlpha = 0.03 + Math.sin(t * 0.3 + blk * 0.7) * 0.02; Draw.rect(ctx, bx, by, bw, bh, H.rgba(bColor, bAlpha)); } // Dense scanlines Draw.scanlines(ctx, w, h, 2, H.rgba('#FFFFFF', 0.03)); // Thicker periodic scanlines for (var tsl = 0; tsl < h; tsl += 16) { Draw.rect(ctx, 0, tsl, w, 1, H.rgba(c[2], 0.02 + Math.sin(t * 0.5 + tsl * 0.01) * 0.01)); } // Horizontal displacement glitch bands var glitchRng = H.seededRandom(Math.floor(t * 2) + 501); if (glitchRng() > 0.6) { for (var gb = 0; gb < 3; gb++) { var gy = glitchRng() * h; var gHeight = 2 + glitchRng() * 8; var gOffset = (glitchRng() - 0.5) * 20; ctx.fillStyle = H.rgba(c[2], 0.08); ctx.fillRect(gOffset, gy, w, gHeight); ctx.fillStyle = H.rgba(c[3], 0.06); ctx.fillRect(-gOffset, gy + 1, w, gHeight * 0.5); } } // Signal meter bars for (var bar = 0; bar < 12; bar++) { var barH = h * 0.03 + Math.sin(t * 2 + bar * 0.5) * h * 0.02; var barColor = bar < 8 ? c[2] : c[3]; Draw.rect(ctx, w * 0.05 + bar * w * 0.04, h * 0.9 - barH, w * 0.03, barH, H.rgba(barColor, 0.15)); } // Crosshair / target var chAlpha = 0.1 + Math.sin(t * 0.6) * 0.04; ctx.strokeStyle = H.rgba(c[2], chAlpha); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(w * 0.5, h * 0.5, 30, 0, Math.PI * 2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(w * 0.5 - 40, h * 0.5); ctx.lineTo(w * 0.5 + 40, h * 0.5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(w * 0.5, h * 0.5 - 40); ctx.lineTo(w * 0.5, h * 0.5 + 40); ctx.stroke(); }, particles: null }, glitch: { meta: { id: 'retro_glitch', mood: 'chaotic', colors: ['#0a0a0a', '#1a0a1a', '#ff0055', '#00ffcc'], tags: ['glitch', 'broken', 'chaotic', 'digital'], description: 'Fragmented display with chromatic aberration, displaced blocks and corrupted data streaks', compatibleMusicMoods: ['chaotic', 'electronic', 'tense'], recommendedFor: ['action', 'racing', 'physics'] }, colors: ['#0a0a0a', '#1a0a1a', '#ff0055', '#00ffcc'], 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(Math.floor(t * 3) + 600); // Displaced rectangular blocks for (var blk = 0; blk < 6; blk++) { var bx = rng() * w; var by = rng() * h; var bw = 20 + rng() * 100; var bh = 3 + rng() * 20; var shift = (rng() - 0.5) * 30; // RGB channel separation Draw.rect(ctx, bx + shift, by, bw, bh, H.rgba(c[2], 0.08 + rng() * 0.1)); Draw.rect(ctx, bx - shift, by + 1, bw, bh, H.rgba(c[3], 0.06 + rng() * 0.08)); } // Static deterministic elements var srng = H.seededRandom(601); // Corrupted horizontal streaks for (var str = 0; str < 15; str++) { var sy = srng() * h; var sx = srng() * w * 0.3; var sw = w * 0.2 + srng() * w * 0.5; var jitter = Math.sin(t * 4 + str * 2) > 0.7 ? (srng() - 0.5) * 10 : 0; Draw.rect(ctx, sx + jitter, sy, sw, 1, H.rgba(srng() > 0.5 ? c[2] : c[3], 0.06)); } // Large chromatic aberration text ctx.font = 'bold 48px monospace'; var errAlpha = 0.06 + Math.sin(t * 1.5) * 0.03; var errShift = 3 + Math.sin(t * 5) * 2; ctx.fillStyle = H.rgba(c[2], errAlpha); ctx.fillText('ERR', w * 0.35 - errShift, h * 0.5); ctx.fillStyle = H.rgba(c[3], errAlpha); ctx.fillText('ERR', w * 0.35 + errShift, h * 0.5 + 2); ctx.fillStyle = H.rgba('#FFFFFF', errAlpha * 0.5); ctx.fillText('ERR', w * 0.35, h * 0.5 + 1); // Data corruption blocks for (var dc = 0; dc < 10; dc++) { var dcx = srng() * w, dcy = srng() * h; var dcw = 4 + srng() * 12, dch = 4 + srng() * 12; var dcPulse = Math.sin(t * 3 + dc * 1.1) > 0.5; if (dcPulse) { Draw.rect(ctx, dcx, dcy, dcw, dch, H.rgba(srng() > 0.5 ? c[2] : c[3], 0.15)); } } // Thin scan lines Draw.scanlines(ctx, w, h, 4, H.rgba('#FFFFFF', 0.02)); // Occasional full-width flash if (Math.sin(t * 0.8) > 0.97) { Draw.rect(ctx, 0, 0, w, h, H.rgba('#FFFFFF', 0.03)); } }, particles: { type: 'particles', count: 15, color: '#FF0055' } }, eightBit: { meta: { id: 'retro_eightBit', mood: 'cheerful', colors: ['#2b2b6e', '#3d3d94', '#5bc0eb', '#fde74c'], tags: ['8bit', 'nintendo', 'cheerful', 'classic'], description: 'A charming 8-bit scene with blocky pixel clouds, hills and a bright primary-color sky', compatibleMusicMoods: ['playful', 'retro', 'cheerful'], recommendedFor: ['action', 'puzzle', 'creative'] }, colors: ['#2b2b6e', '#3d3d94', '#5bc0eb', '#fde74c'], renderBase: function(ctx, w, h, c, Draw, t) { // Bright 8-bit sky Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90); }, renderMid: function(ctx, w, h, c, Draw, t) { var rng = H.seededRandom(700); var px = 8; // pixel size for blocky look // Pixel sun var sunX = w * 0.75, sunY = h * 0.2; var sunPx = [ [-1,-2],[0,-2],[1,-2], [-2,-1],[-1,-1],[0,-1],[1,-1],[2,-1], [-2,0],[-1,0],[0,0],[1,0],[2,0], [-2,1],[-1,1],[0,1],[1,1],[2,1], [-1,2],[0,2],[1,2] ]; for (var sp = 0; sp < sunPx.length; sp++) { Draw.rect(ctx, sunX + sunPx[sp][0] * px, sunY + sunPx[sp][1] * px, px, px, c[3]); } // Pixel clouds var cloudPositions = [{ x: 0.15, y: 0.18 }, { x: 0.45, y: 0.12 }, { x: 0.8, y: 0.22 }]; var cloudShape = [[0,0],[1,0],[2,0],[3,0],[-1,1],[0,1],[1,1],[2,1],[3,1],[4,1],[0,2],[1,2],[2,2],[3,2]]; for (var cl = 0; cl < cloudPositions.length; cl++) { var clx = cloudPositions[cl].x * w + Math.sin(t * 0.3 + cl) * 5; var cly = cloudPositions[cl].y * h; for (var cp = 0; cp < cloudShape.length; cp++) { Draw.rect(ctx, clx + cloudShape[cp][0] * px, cly + cloudShape[cp][1] * px, px, px, '#FFFFFF'); } } // Pixel hills (green terrain at bottom) var groundY = h * 0.7; Draw.rect(ctx, 0, groundY, w, h - groundY, '#4CAF50'); // Stepped hill shapes var hillRng = H.seededRandom(701); for (var hill = 0; hill < 5; hill++) { var hx = hillRng() * w; var hPeakH = 4 + Math.floor(hillRng() * 6); for (var hr = 0; hr < hPeakH; hr++) { var hRowW = (hPeakH - hr) * 2 + 1; for (var hc = 0; hc < hRowW; hc++) { Draw.rect(ctx, hx + (hc - Math.floor(hRowW / 2)) * px, groundY - hr * px, px, px, '#388E3C'); } } } // Ground detail for (var gd = 0; gd < 15; gd++) { var gdx = Math.floor(rng() * (w / px)) * px; var gdy = groundY + Math.floor(rng() * ((h - groundY) / px)) * px; Draw.rect(ctx, gdx, gdy, px, px, H.rgba('#2E7D32', 0.5)); } // Small pixel flowers var flowerColors = ['#FF5252', '#FFD740', '#E040FB', '#FFFFFF']; for (var fl = 0; fl < 8; fl++) { var flx = Math.floor(rng() * (w / px)) * px; var fly = groundY + px + Math.floor(rng() * 3) * px; Draw.rect(ctx, flx, fly, px, px, flowerColors[fl % flowerColors.length]); } // Coin block (floating, animated) var coinY = h * 0.5 + Math.sin(t * 2) * 2; var blockPulse = Math.sin(t * 1.5) > 0.8 ? '#FFD740' : c[3]; Draw.rect(ctx, w * 0.5, coinY, px * 2, px * 2, blockPulse); ctx.strokeStyle = H.rgba('#000', 0.3); ctx.lineWidth = 1; ctx.strokeRect(w * 0.5, coinY, px * 2, px * 2); Draw.rect(ctx, w * 0.5 + 3, coinY + 3, px * 2 - 6, px * 2 - 6, H.darken(blockPulse, 0.2)); }, particles: null }, synthwave: { meta: { id: 'retro_synthwave', mood: 'epic', colors: ['#0d0015', '#1a0033', '#ff2975', '#f222ff'], tags: ['synthwave', 'outrun', 'neon', 'night'], description: 'An outrun-style neon horizon with a receding road, mountain silhouettes and electric sky', compatibleMusicMoods: ['epic', 'energetic', 'electronic'], recommendedFor: ['racing', 'action', 'sports'] }, colors: ['#0d0015', '#1a0033', '#ff2975', '#f222ff'], renderBase: function(ctx, w, h, c, Draw, t) { // Dark sky with neon gradient at horizon var skyGrad = Draw.linearGradient(ctx, 0, 0, 0, h * 0.55, [ [0, c[0]], [0.5, c[1]], [0.85, '#330044'], [1, '#ff297530'] ]); ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, w, h * 0.55); Draw.stars(ctx, w, h * 0.4, 70, '#FFFFFF', 801, [0.5, 1.5]); // Neon sun (half visible at horizon) var sunCy = h * 0.53; var sunGrad = Draw.linearGradient(ctx, 0, sunCy - 50, 0, sunCy + 50, [ [0, c[2]], [0.5, c[3]], [1, '#FF6600'] ]); ctx.fillStyle = sunGrad; ctx.beginPath(); ctx.arc(w * 0.5, sunCy, 55, Math.PI, 0); ctx.fill(); // Sun horizontal lines for (var sl = 0; sl < 5; sl++) { var sly = sunCy - 45 + sl * 12; Draw.rect(ctx, w * 0.5 - 55, sly, 110, 3 + sl, c[0]); } Draw.glow(ctx, w * 0.5, sunCy, 100, H.rgba(c[2], 0.1)); }, renderMid: function(ctx, w, h, c, Draw, t) { var horizonY = h * 0.55; // Mountain silhouettes Draw.mountains(ctx, w, h, horizonY, 5, '#0a000f', 802); // Ground plane Draw.rect(ctx, 0, horizonY, w, h - horizonY, '#05000a'); // Perspective grid - horizontal lines (receding, animated) var gridOffset = (t * 20) % 30; ctx.strokeStyle = H.rgba(c[3], 0.25); ctx.lineWidth = 1; for (var gy = 0; gy < 25; gy++) { var gridFrac = (gy + gridOffset / 30) / 25; var lineY = horizonY + gridFrac * gridFrac * (h - horizonY); var lineAlpha = gridFrac * 0.3; ctx.strokeStyle = H.rgba(c[3], lineAlpha); ctx.beginPath(); ctx.moveTo(0, lineY); ctx.lineTo(w, lineY); ctx.stroke(); } // Vertical converging lines ctx.strokeStyle = H.rgba(c[2], 0.2); for (var gx = -10; gx <= 10; gx++) { var topX = w * 0.5; var botX = w * 0.5 + gx * w * 0.065; ctx.beginPath(); ctx.moveTo(topX, horizonY); ctx.lineTo(botX, h); ctx.stroke(); } // Road center stripe for (var rs = 0; rs < 12; rs++) { var rsFrac = rs / 12; var rsY = horizonY + rsFrac * rsFrac * (h - horizonY); var rsW = 2 + rsFrac * 6; var rsH = 3 + rsFrac * 8; var rsAlpha = 0.3 + rsFrac * 0.4; Draw.rect(ctx, w * 0.5 - rsW / 2, rsY, rsW, rsH, H.rgba('#FFFFFF', rsAlpha)); } // Neon side lights along road for (var nl = 0; nl < 6; nl++) { var nlFrac = (nl + 0.5) / 6; var nlY = horizonY + nlFrac * nlFrac * (h - horizonY); var spread = nlFrac * w * 0.3; var nlSize = 2 + nlFrac * 5; var nlPulse = 0.4 + Math.sin(t * 2 + nl) * 0.2; // Left Draw.glow(ctx, w * 0.5 - spread, nlY, nlSize * 3, H.rgba(c[2], nlPulse * 0.3)); Draw.circle(ctx, w * 0.5 - spread, nlY, nlSize, H.rgba(c[2], nlPulse)); // Right Draw.glow(ctx, w * 0.5 + spread, nlY, nlSize * 3, H.rgba(c[3], nlPulse * 0.3)); Draw.circle(ctx, w * 0.5 + spread, nlY, nlSize, H.rgba(c[3], nlPulse)); } // Horizon glow line ctx.strokeStyle = H.rgba(c[2], 0.35 + Math.sin(t * 0.8) * 0.1); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, horizonY); ctx.lineTo(w, horizonY); ctx.stroke(); }, particles: { type: 'stars', count: 20, color: '#F222FF' } } }); })(window.BackgroundEngine);