1// sparkles.js â Sparkle particles over gold pile and mushrooms 2 3'use strict'; 4 5(function () { 6 const canvas = document.getElementById('nl-sparkles'); 7 if (!canvas) return; 8 9 const ctx = canvas.getContext('2d'); 10 11 // Three spawn zones with their own palettes 12 const ZONES = [ 13 { 14 // Left â blue/teal mushrooms 15 cx: 0.16, 16 spread: 0.10, 17 cy: 0.72, 18 colors: ['#a8e8ff', '#70d4ff', '#c8f4ff', '#40c8f0', '#e0f8ff', '#88e0ff'], 19 }, 20 { 21 // Center â gold coin pile 22 cx: 0.48, 23 spread: 0.14, 24 cy: 0.88, 25 colors: ['#ffe066', '#ffd700', '#e8c8ff', '#fff4a0', '#c8a8ff', '#fffad0', '#d4b8ff', '#ffeeaa'], 26 }, 27 { 28 // Right â rainbow mushrooms (red + green) 29 cx: 0.82, 30 spread: 0.10, 31 cy: 0.72, 32 colors: ['#ff6060', '#ff3030', '#80ff80', '#40e840', '#ffaaaa', '#aaffaa', '#ff8080', '#60ff60'], 33 }, 34 ]; 35 36 const particles = []; 37 38 function resize() { 39 canvas.width = canvas.offsetWidth; 40 canvas.height = canvas.offsetHeight; 41 } 42 43 function spawnFromZone(zone) { 44 const size = 1.2 + Math.random() * 2.6; 45 particles.push({ 46 x: canvas.width * zone.cx + (Math.random() - 0.5) * canvas.width * zone.spread, 47 y: canvas.height * zone.cy + (Math.random() - 0.5) * canvas.height * 0.06, 48 vx: (Math.random() - 0.5) * 0.18, 49 vy: -(0.10 + Math.random() * 0.28), 50 size, 51 maxSize: size, 52 life: 0, 53 maxLife: 240 + Math.random() * 200, 54 color: zone.colors[Math.floor(Math.random() * zone.colors.length)], 55 twinkle: Math.random() * Math.PI * 2, 56 }); 57 } 58 59 function drawStar(x, y, r, opacity, color) { 60 ctx.save(); 61 ctx.translate(x, y); 62 63 // Soft outer glow 64 const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, r * 4); 65 grd.addColorStop(0, hexToRgba(color, opacity * 0.5)); 66 grd.addColorStop(0.4, hexToRgba(color, opacity * 0.15)); 67 grd.addColorStop(1, hexToRgba(color, 0)); 68 ctx.beginPath(); 69 ctx.arc(0, 0, r * 4, 0, Math.PI * 2); 70 ctx.fillStyle = grd; 71 ctx.fill(); 72 73 // 4-point star 74 ctx.globalAlpha = opacity; 75 ctx.beginPath(); 76 for (let i = 0; i < 8; i++) { 77 const angle = (i * Math.PI) / 4 - Math.PI / 4; 78 const radius = i % 2 === 0 ? r : r * 0.35; 79 i === 0 80 ? ctx.moveTo(Math.cos(angle) * radius, Math.sin(angle) * radius) 81 : ctx.lineTo(Math.cos(angle) * radius, Math.sin(angle) * radius); 82 } 83 ctx.closePath(); 84 ctx.fillStyle = '#ffffff'; 85 ctx.shadowColor = color; 86 ctx.shadowBlur = r * 3; 87 ctx.fill(); 88 89 ctx.restore(); 90 } 91 92 function hexToRgba(hex, alpha) { 93 const r = parseInt(hex.slice(1,3), 16); 94 const g = parseInt(hex.slice(3,5), 16); 95 const b = parseInt(hex.slice(5,7), 16); 96 return `rgba(${r},${g},${b},${alpha})`; 97 } 98 99 let frame = 0; 100 101 function tick() { 102 requestAnimationFrame(tick); 103 if (window._svScrolling) return; // yield GPU to scroll compositor 104 ctx.clearRect(0, 0, canvas.width, canvas.height); 105 106 frame++; 107 // Spawn one particle per zone every ~20 frames, staggered 108 ZONES.forEach((zone, i) => { 109 if (frame % 20 === i * 6 % 20) spawnFromZone(zone); 110 }); 111 112 for (let i = particles.length - 1; i >= 0; i--) { 113 const p = particles[i]; 114 p.life++; 115 p.x += p.vx; 116 p.y += p.vy; 117 p.vy -= 0.002; 118 119 const progress = p.life / p.maxLife; 120 121 let opacity; 122 if (progress < 0.15) opacity = progress / 0.15; 123 else if (progress < 0.60) opacity = 1; 124 else opacity = 1 - (progress - 0.6) / 0.4; 125 126 const twinkle = 0.75 + 0.25 * Math.sin(p.twinkle + p.life * 0.06); 127 const r = p.maxSize * twinkle * (1 - progress * 0.3); 128 129 drawStar(p.x, p.y, r, opacity * 0.85, p.color); 130 131 if (p.life >= p.maxLife) particles.splice(i, 1); 132 } 133 } 134 135 window.addEventListener('resize', resize); 136 resize(); 137 tick(); 138})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.