Files
gamercomp/frontend/public/assets/backgrounds/themes/retro.js
T
2026-04-30 02:14:25 +00:00

552 lines
26 KiB
JavaScript

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