// abstract.js — Abstract theme backgrounds // Variants: geometricPatterns, gradients, particleFields, minimalist, waves, fractals, kaleidoscope, matrix (function(engine) { if (!engine) return; var D = engine.Draw, H = engine.helpers; engine.registerTheme('abstract', { geometricPatterns: { meta: { id: 'abstract_geometricPatterns', mood: 'focused', colors: ['#0d1b2a', '#1b2838', '#e07a5f', '#f2cc8f'], tags: ['geometric', 'shapes', 'patterns', 'clean'], description: 'Interlocking geometric shapes in warm contrasting tones on a deep navy canvas', compatibleMusicMoods: ['focused', 'calm', 'electronic'], recommendedFor: ['puzzle', 'trivia', 'creative'] }, colors: ['#0d1b2a', '#1b2838', '#e07a5f', '#f2cc8f'], renderBase: function(ctx, w, h, c, Draw, t) { Draw.fillGradient(ctx, w, h, [c[0], c[1]], 135); }, renderMid: function(ctx, w, h, c, Draw, t) { var rng = H.seededRandom(100); // Rotating hexagonal grid var spacing = 80; for (var row = -1; row < h / spacing + 1; row++) { for (var col = -1; col < w / spacing + 1; col++) { var hx = col * spacing + (row % 2 === 0 ? spacing / 2 : 0); var hy = row * spacing; var dist = Math.sqrt(Math.pow(hx - w / 2, 2) + Math.pow(hy - h / 2, 2)); var alpha = H.clamp(0.08 + Math.sin(t * 0.5 + dist * 0.005) * 0.06, 0, 0.2); var rot = t * 0.1 + dist * 0.002; var colorChoice = rng() > 0.5 ? c[2] : c[3]; Draw.polygon(ctx, hx, hy, 25 + Math.sin(t * 0.3 + dist * 0.01) * 5, 6, H.rgba(colorChoice, alpha), rot); } } // Accent triangles for (var tri = 0; tri < 12; tri++) { var tx = rng() * w, ty = rng() * h; var tSize = 20 + rng() * 40; var tRot = t * 0.2 * (rng() > 0.5 ? 1 : -1) + rng() * Math.PI * 2; var tAlpha = 0.1 + Math.sin(t * 0.7 + tri) * 0.05; Draw.polygon(ctx, tx, ty, tSize, 3, H.rgba(c[2], tAlpha), tRot); } // Connecting lines ctx.strokeStyle = H.rgba(c[3], 0.06); ctx.lineWidth = 1; var lrng = H.seededRandom(101); for (var ln = 0; ln < 15; ln++) { ctx.beginPath(); ctx.moveTo(lrng() * w, lrng() * h); ctx.lineTo(lrng() * w, lrng() * h); ctx.stroke(); } }, particles: { type: 'dust', count: 20, color: '#F2CC8F' } }, gradients: { meta: { id: 'abstract_gradients', mood: 'dreamy', colors: ['#0f0c29', '#302b63', '#24243e', '#ff6b6b'], tags: ['gradient', 'smooth', 'colorful', 'flowing'], description: 'Smoothly blending gradient orbs that shift and pulse with ethereal color transitions', compatibleMusicMoods: ['dreamy', 'calm', 'ambient'], recommendedFor: ['creative', 'story', 'music'] }, colors: ['#0f0c29', '#302b63', '#24243e', '#ff6b6b'], renderBase: function(ctx, w, h, c, Draw, t) { Draw.fillGradient(ctx, w, h, [c[0], c[1], c[2]], 90); }, renderMid: function(ctx, w, h, c, Draw, t) { var rng = H.seededRandom(200); // Animated gradient orbs var orbs = [ { x: 0.3, y: 0.3, color: c[3], r: 0.25 }, { x: 0.7, y: 0.6, color: '#6b5bff', r: 0.3 }, { x: 0.5, y: 0.8, color: '#ff9a76', r: 0.2 }, { x: 0.2, y: 0.7, color: '#a66bff', r: 0.22 }, { x: 0.8, y: 0.2, color: '#ff6bb5', r: 0.18 } ]; ctx.globalCompositeOperation = 'screen'; for (var i = 0; i < orbs.length; i++) { var orb = orbs[i]; var ox = orb.x * w + Math.sin(t * 0.3 + i * 1.5) * w * 0.05; var oy = orb.y * h + Math.cos(t * 0.2 + i * 1.2) * h * 0.05; var or = orb.r * Math.min(w, h) * (1 + Math.sin(t * 0.4 + i) * 0.1); var grad = Draw.radialGradient(ctx, ox, oy, or, [ [0, H.rgba(orb.color, 0.3)], [0.5, H.rgba(orb.color, 0.1)], [1, H.rgba(orb.color, 0)] ]); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(ox, oy, or, 0, Math.PI * 2); ctx.fill(); } ctx.globalCompositeOperation = 'source-over'; // Subtle mesh lines ctx.strokeStyle = H.rgba('#FFFFFF', 0.02); ctx.lineWidth = 1; for (var ml = 0; ml < 8; ml++) { var my = h * 0.1 + ml * h * 0.1; ctx.beginPath(); for (var mx = 0; mx < w; mx += 5) { var mdy = my + Math.sin(mx * 0.01 + t * 0.5 + ml * 0.5) * 15; mx === 0 ? ctx.moveTo(mx, mdy) : ctx.lineTo(mx, mdy); } ctx.stroke(); } }, particles: { type: 'sparkles', count: 15, color: '#FF6B6B' } }, particleFields: { meta: { id: 'abstract_particleFields', mood: 'energetic', colors: ['#000000', '#111122', '#00ff88', '#0088ff'], tags: ['particles', 'field', 'connected', 'network'], description: 'A dynamic network of connected particle nodes with pulsing energy links', compatibleMusicMoods: ['energetic', 'electronic', 'focused'], recommendedFor: ['action', 'puzzle', 'physics'] }, colors: ['#000000', '#111122', '#00ff88', '#0088ff'], 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(300); // Generate node positions (deterministic) var nodes = []; for (var n = 0; n < 30; n++) { var baseX = rng() * w, baseY = rng() * h; nodes.push({ x: baseX + Math.sin(t * 0.5 + n * 0.7) * 15, y: baseY + Math.cos(t * 0.4 + n * 0.9) * 15, size: 2 + rng() * 3, color: rng() > 0.5 ? c[2] : c[3] }); } // Draw connections between nearby nodes var maxDist = 150; for (var i = 0; i < nodes.length; i++) { for (var j = i + 1; j < nodes.length; j++) { var dx = nodes[i].x - nodes[j].x; var dy = nodes[i].y - nodes[j].y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < maxDist) { var lineAlpha = (1 - dist / maxDist) * 0.2; var pulse = 0.5 + Math.sin(t * 1.5 + i + j) * 0.5; ctx.strokeStyle = H.rgba(nodes[i].color, lineAlpha * pulse); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.stroke(); } } } // Draw nodes for (var k = 0; k < nodes.length; k++) { var nd = nodes[k]; var nodePulse = 0.6 + Math.sin(t * 2 + k * 0.5) * 0.3; Draw.glow(ctx, nd.x, nd.y, nd.size * 4, H.rgba(nd.color, 0.15 * nodePulse)); Draw.circle(ctx, nd.x, nd.y, nd.size, H.rgba(nd.color, 0.7 * nodePulse)); } // Data stream accents along bottom ctx.strokeStyle = H.rgba(c[2], 0.04); ctx.lineWidth = 1; for (var ds = 0; ds < 5; ds++) { var dsy = h * 0.85 + ds * 8; ctx.beginPath(); for (var dx2 = 0; dx2 < w; dx2 += 3) { var ddy = dsy + Math.sin(dx2 * 0.03 + t * 2 + ds) * 3; dx2 === 0 ? ctx.moveTo(dx2, ddy) : ctx.lineTo(dx2, ddy); } ctx.stroke(); } }, particles: { type: 'particles', count: 15, color: '#00FF88' } }, minimalist: { meta: { id: 'abstract_minimalist', mood: 'calm', colors: ['#f5f5f0', '#e8e4dd', '#2d2d2d', '#c44536'], tags: ['minimal', 'clean', 'simple', 'elegant'], description: 'Clean open space with a single bold accent line and carefully placed geometric forms', compatibleMusicMoods: ['calm', 'ambient', 'focused'], recommendedFor: ['puzzle', 'trivia', 'creative'] }, colors: ['#f5f5f0', '#e8e4dd', '#2d2d2d', '#c44536'], 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) { // Single horizon line ctx.strokeStyle = H.rgba(c[2], 0.15); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, h * 0.618); ctx.lineTo(w, h * 0.618); ctx.stroke(); // Accent circle var circX = w * 0.7, circY = h * 0.35; var circR = Math.min(w, h) * 0.08; var breathe = 1 + Math.sin(t * 0.5) * 0.03; Draw.circle(ctx, circX, circY, circR * breathe, H.rgba(c[3], 0.7)); // Shadow beneath circle ctx.fillStyle = H.rgba(c[2], 0.04); ctx.beginPath(); ctx.ellipse(circX, h * 0.618, circR * 1.5, 4, 0, 0, Math.PI * 2); ctx.fill(); // Thin accent line from circle ctx.strokeStyle = H.rgba(c[3], 0.2); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(circX, circY + circR * breathe); ctx.lineTo(circX, h * 0.618); ctx.stroke(); // Sparse dots var rng = H.seededRandom(400); for (var d = 0; d < 5; d++) { var dx = rng() * w, dy = rng() * h; Draw.circle(ctx, dx, dy, 2, H.rgba(c[2], 0.08)); } // Small typography-like lines var lx = w * 0.15; for (var l = 0; l < 4; l++) { var lw = 30 + rng() * 60; ctx.fillStyle = H.rgba(c[2], 0.06); ctx.fillRect(lx, h * 0.72 + l * 12, lw, 2); } }, particles: null }, waves: { meta: { id: 'abstract_waves', mood: 'flowing', colors: ['#0a192f', '#172a45', '#00b4d8', '#90e0ef'], tags: ['waves', 'ocean', 'flowing', 'smooth'], description: 'Layered sinusoidal waves undulating in cool blue tones with foam accents', compatibleMusicMoods: ['calm', 'flowing', 'ambient'], recommendedFor: ['creative', 'music', 'physics'] }, colors: ['#0a192f', '#172a45', '#00b4d8', '#90e0ef'], 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) { // Layered wave bands var waveLayers = [ { y: 0.3, amp: 25, freq: 0.008, speed: 0.4, color: c[2], alpha: 0.08 }, { y: 0.4, amp: 30, freq: 0.006, speed: 0.3, color: c[2], alpha: 0.1 }, { y: 0.5, amp: 20, freq: 0.01, speed: 0.5, color: c[3], alpha: 0.12 }, { y: 0.6, amp: 35, freq: 0.005, speed: 0.25, color: c[2], alpha: 0.15 }, { y: 0.7, amp: 15, freq: 0.012, speed: 0.6, color: c[3], alpha: 0.1 }, { y: 0.8, amp: 40, freq: 0.004, speed: 0.2, color: c[2], alpha: 0.18 } ]; for (var i = 0; i < waveLayers.length; i++) { var wl = waveLayers[i]; ctx.fillStyle = H.rgba(wl.color, wl.alpha); ctx.beginPath(); ctx.moveTo(0, h); for (var x = 0; x <= w; x += 3) { var y = wl.y * h + Math.sin(x * wl.freq + t * wl.speed) * wl.amp + Math.sin(x * wl.freq * 2.3 + t * wl.speed * 1.5) * wl.amp * 0.3; ctx.lineTo(x, y); } ctx.lineTo(w, h); ctx.closePath(); ctx.fill(); } // Foam highlights on top wave ctx.strokeStyle = H.rgba('#FFFFFF', 0.06); ctx.lineWidth = 1.5; ctx.beginPath(); for (var fx = 0; fx <= w; fx += 3) { var fy = 0.3 * h + Math.sin(fx * 0.008 + t * 0.4) * 25; fx === 0 ? ctx.moveTo(fx, fy) : ctx.lineTo(fx, fy); } ctx.stroke(); }, particles: { type: 'bubbles', count: 15, color: '#90E0EF' } }, fractals: { meta: { id: 'abstract_fractals', mood: 'complex', colors: ['#0a0a0a', '#1a1a2e', '#e94560', '#0f3460'], tags: ['fractal', 'recursive', 'mathematical', 'complex'], description: 'Recursive branching patterns that grow and pulse like living mathematical organisms', compatibleMusicMoods: ['complex', 'electronic', 'mystical'], recommendedFor: ['puzzle', 'physics', 'creative'] }, colors: ['#0a0a0a', '#1a1a2e', '#e94560', '#0f3460'], 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) { // Recursive tree fractal function branch(x, y, len, angle, depth, maxDepth) { if (depth > maxDepth || len < 3) return; var endX = x + Math.cos(angle) * len; var endY = y + Math.sin(angle) * len; var alpha = 0.15 * (1 - depth / maxDepth) + Math.sin(t * 0.5 + depth) * 0.03; var lw = (maxDepth - depth) * 0.8 + 0.5; ctx.strokeStyle = H.rgba(depth % 2 === 0 ? c[2] : c[3], alpha); ctx.lineWidth = lw; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(endX, endY); ctx.stroke(); var spread = 0.4 + Math.sin(t * 0.3 + depth * 0.5) * 0.1; branch(endX, endY, len * 0.7, angle - spread, depth + 1, maxDepth); branch(endX, endY, len * 0.7, angle + spread, depth + 1, maxDepth); } // Main tree from bottom center branch(w * 0.5, h * 0.85, h * 0.18, -Math.PI / 2, 0, 9); // Smaller trees branch(w * 0.2, h * 0.9, h * 0.1, -Math.PI / 2, 0, 7); branch(w * 0.8, h * 0.9, h * 0.12, -Math.PI / 2, 0, 7); // Spiral accents var rng = H.seededRandom(500); for (var sp = 0; sp < 4; sp++) { var sx = rng() * w, sy = rng() * h * 0.6; ctx.strokeStyle = H.rgba(c[2], 0.06); ctx.lineWidth = 1; ctx.beginPath(); for (var sa = 0; sa < Math.PI * 6; sa += 0.1) { var sr = sa * 2 + Math.sin(t * 0.3 + sp) * 2; var ssx = sx + Math.cos(sa) * sr; var ssy = sy + Math.sin(sa) * sr; sa === 0 ? ctx.moveTo(ssx, ssy) : ctx.lineTo(ssx, ssy); } ctx.stroke(); } }, particles: { type: 'sparkles', count: 20, color: '#E94560' } }, kaleidoscope: { meta: { id: 'abstract_kaleidoscope', mood: 'playful', colors: ['#1a1a2e', '#16213e', '#e94560', '#ffd166'], tags: ['kaleidoscope', 'symmetry', 'colorful', 'playful'], description: 'Symmetric radial patterns in vibrant colors rotating like a living kaleidoscope', compatibleMusicMoods: ['playful', 'energetic', 'electronic'], recommendedFor: ['music', 'creative', 'puzzle'] }, colors: ['#1a1a2e', '#16213e', '#e94560', '#ffd166'], 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 cx = w / 2, cy = h / 2; var segments = 8; var rng = H.seededRandom(600); ctx.save(); ctx.translate(cx, cy); // Draw mirrored segments for (var seg = 0; seg < segments; seg++) { var segAngle = (seg / segments) * Math.PI * 2 + t * 0.1; ctx.save(); ctx.rotate(segAngle); // Shapes within each segment for (var s = 0; s < 5; s++) { var sd = 40 + s * 35 + Math.sin(t * 0.4 + s) * 10; var sSize = 8 + rng() * 15; var sColor = s % 2 === 0 ? c[2] : c[3]; var sAlpha = 0.12 + Math.sin(t * 0.6 + s * 0.8) * 0.05; Draw.polygon(ctx, sd, 0, sSize, 3 + (s % 3), H.rgba(sColor, sAlpha), t * 0.2 + s); } // Connecting arcs ctx.strokeStyle = H.rgba(c[2], 0.06); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0, 0, 80 + Math.sin(t * 0.5) * 10, 0, Math.PI / segments); ctx.stroke(); ctx.restore(); } ctx.restore(); // Central glow var centerPulse = 0.15 + Math.sin(t * 0.8) * 0.05; Draw.glow(ctx, cx, cy, 60, H.rgba(c[3], centerPulse)); Draw.glow(ctx, cx, cy, 30, H.rgba(c[2], centerPulse * 1.5)); // Outer ring ctx.strokeStyle = H.rgba(c[3], 0.08 + Math.sin(t * 0.3) * 0.03); ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, Math.min(w, h) * 0.35, 0, Math.PI * 2); ctx.stroke(); }, particles: { type: 'sparkles', count: 25, color: '#FFD166' } }, matrix: { meta: { id: 'abstract_matrix', mood: 'digital', colors: ['#000000', '#001100', '#00ff41', '#008f11'], tags: ['matrix', 'code', 'digital', 'hacker'], description: 'Cascading columns of digital characters streaming down like the matrix rain', compatibleMusicMoods: ['dark', 'electronic', 'tense'], recommendedFor: ['action', 'puzzle', 'trivia'] }, colors: ['#000000', '#001100', '#00ff41', '#008f11'], renderBase: function(ctx, w, h, c, Draw, t) { Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90); // CRT vignette var vignette = Draw.radialGradient(ctx, w / 2, h / 2, Math.max(w, h) * 0.7, [ [0, H.rgba(c[1], 0)], [0.7, H.rgba(c[0], 0.3)], [1, H.rgba(c[0], 0.8)] ]); ctx.fillStyle = vignette; ctx.fillRect(0, 0, w, h); }, renderMid: function(ctx, w, h, c, Draw, t) { var rng = H.seededRandom(700); var columns = Math.floor(w / 16); var chars = '01アイウエオカキクケコサシスセソ{}[]<>+=/*#@$%&'; ctx.font = '13px monospace'; // Column rain for (var col = 0; col < columns; col++) { var cx = col * 16 + 4; var speed = 0.3 + rng() * 0.7; var colOffset = rng() * h; var colLen = 8 + Math.floor(rng() * 20); // Calculate head position var headY = ((t * speed * 60 + colOffset) % (h + colLen * 16)) - colLen * 16; for (var ch = 0; ch < colLen; ch++) { var cy = headY + ch * 16; if (cy < -16 || cy > h + 16) continue; var charIdx = Math.floor(rng() * chars.length); // Head is brightest, tail fades var fade = 1 - (ch / colLen); if (ch === 0) { ctx.fillStyle = H.rgba('#FFFFFF', 0.8); } else { ctx.fillStyle = H.rgba(c[2], fade * 0.5 + Math.sin(t * 3 + col + ch) * 0.08); } ctx.fillText(chars[charIdx], cx, cy); } } // Occasional bright flashes if (Math.sin(t * 1.3) > 0.95) { var flashX = Math.floor(rng() * columns) * 16; var flashY = rng() * h; Draw.glow(ctx, flashX, flashY, 30, H.rgba(c[2], 0.3)); } // Subtle horizontal scan line var scanY = (t * 40) % h; ctx.fillStyle = H.rgba(c[2], 0.03); ctx.fillRect(0, scanY, w, 2); }, particles: null } }); })(window.BackgroundEngine);