1/* Overlapping decorative bursts, behind the homepage text and controls. */ 2(() => { 3 const hero = document.querySelector('.home-hero'); 4 if (!hero || hero.querySelector('.hero-bursts')) return; 5 const reduced = matchMedia('(prefers-reduced-motion: reduce)'); 6 const canvas = document.createElement('canvas'); 7 canvas.className = 'hero-bursts'; 8 canvas.setAttribute('aria-hidden', 'true'); 9 canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;z-index:-2;pointer-events:none;'; 10 const ctx = canvas.getContext('2d'); 11 if (!ctx) return; 12 hero.insertBefore(canvas, hero.querySelector('.hero-shade')); 13 const toggle = document.createElement('button'); 14 toggle.type = 'button'; 15 toggle.style.cssText = 'position:absolute;bottom:12px;left:24px;z-index:3;background:#04112599;border:1px solid #ffffff30;border-radius:4px;color:#d8e2ed;padding:6px 10px;font:12px Arial,sans-serif;'; 16 hero.append(toggle); 17 let paused = false, visible = false, frame = 0, timer = 0, width = 0, height = 0, turn = 0; 18 let bursts = [], nextBurst = 0; 19 const enabled = () => !paused && !reduced.matches && visible && !document.hidden; 20 const clear = () => { cancelAnimationFrame(frame); clearTimeout(timer); frame = timer = 0; bursts = []; nextBurst = 0; ctx.clearRect(0, 0, width, height); }; 21 const resize = () => { 22 const bounds = hero.getBoundingClientRect(); 23 width = bounds.width; height = bounds.height; 24 const ratio = Math.min(devicePixelRatio || 1, 1.5); 25 canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio); 26 ctx.setTransform(ratio, 0, 0, ratio, 0, 0); 27 }; 28 const spawnBurst = now => { 29 const mobile = width < 600; 30 const positions = mobile ? [[.75,.13],[.22,.3],[.65,.42]] : [[.57,.18],[.84,.3],[.28,.12],[.7,.43],[.14,.33]]; 31 const [px, py] = positions[turn++ % positions.length]; 32 const count = mobile ? 36 : 64; 33 bursts.push({ 34 x: width * (px + (Math.random()-.5)*.06), y: height * py, 35 radius: Math.min(width * .26, 230) * (.75+Math.random()*.25), 36 start: now, duration: 3200 + Math.random()*600, 37 sparks: Array.from({length: count}, (_, i) => ({ 38 angle: i * Math.PI * 2 / count + Math.random()*.05, 39 length: .55 + Math.random()*.45, 40 warmth: i % 4 ? '255,196,125' : '255,239,210' 41 })) 42 }); 43 }; 44 const burst = () => { 45 if (!enabled() || !width || !height) return; 46 const draw = now => { 47 if (!enabled()) { clear(); return; } 48 if (now >= nextBurst) { 49 spawnBurst(now); 50 nextBurst = now + (width < 600 ? 1050 : 650) + Math.random()*300; 51 } 52 ctx.clearRect(0, 0, width, height); 53 bursts = bursts.filter(b => now - b.start < b.duration); 54 ctx.lineCap = 'round'; 55 for (const b of bursts) { 56 const t = (now-b.start)/b.duration; 57 const travel = 1-Math.pow(1-t,3); 58 const opacity = Math.min(1,t/.1)*Math.pow(1-t,1.25); 59 for (const spark of b.sparks) { 60 const r = b.radius*spark.length*travel; 61 const tail = Math.max(0,r-b.radius*.18*(1-t)); 62 const fall = 40*t*t; 63 const x1=b.x+Math.cos(spark.angle)*tail, y1=b.y+Math.sin(spark.angle)*tail+fall; 64 const x2=b.x+Math.cos(spark.angle)*r, y2=b.y+Math.sin(spark.angle)*r+fall; 65 const gradient=ctx.createLinearGradient(x1,y1,x2,y2); 66 gradient.addColorStop(0,'rgba('+spark.warmth+',0)'); 67 gradient.addColorStop(1,'rgba('+spark.warmth+','+opacity+')'); 68 ctx.strokeStyle=gradient;ctx.lineWidth=1.7; 69 ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke(); 70 } 71 } 72 frame=requestAnimationFrame(draw); 73 }; 74 frame=requestAnimationFrame(draw); 75 }; 76 const sync = () => { 77 clear(); canvas.hidden = reduced.matches; toggle.hidden = reduced.matches; 78 toggle.textContent = paused ? 'Play effects' : 'Pause effects'; 79 toggle.setAttribute('aria-label', paused ? 'Play decorative fireworks' : 'Pause decorative fireworks'); 80 if (enabled()) timer = setTimeout(burst, 350); 81 }; 82 toggle.addEventListener('click', () => { paused = !paused; sync(); }); 83 reduced.addEventListener('change', sync); 84 document.addEventListener('visibilitychange', sync); 85 new IntersectionObserver(entries => { visible = entries[0].isIntersecting; sync(); }, {threshold: .08}).observe(hero); 86 new ResizeObserver(() => { resize(); sync(); }).observe(hero); 87 window.addEventListener('pagehide', clear); 88 resize();sync(); 89})(); 90
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.