1(() => { 2 "use strict"; 3 const environment = document.querySelector(".hero-environment"); 4 const hero = document.querySelector("#hero"); 5 if (!environment || !hero) return; 6 const canvas = document.createElement("canvas"); 7 canvas.className = "flight-field"; canvas.setAttribute("aria-hidden", "true"); 8 environment.append(canvas); 9 const ctx = canvas.getContext("2d", { alpha: true }); 10 if (!ctx) return; 11 const reduced = matchMedia("(prefers-reduced-motion: reduce)"); 12 let enabled = !reduced.matches && !document.documentElement.classList.contains("preview-effects-off"); 13 let visible = true, raf = 0, previous = 0, time = 0, painted = 0; 14 let width = 1, height = 1, ratio = 1, worldScale = 1, offsetX = 0, offsetY = 0; 15 const particles = [], flights = [], rings = [], plumes = []; 16 let flightCount = 0, nextFlight = 7800; 17 const ridge = [[0,560],[200,582],[420,592],[560,574],[635,550],[745,578],[834,575],[920,583],[1110,586],[1240,566],[1330,547],[1424,507],[1490,548],[1536,564]]; 18 const rand = (a,b) => a + Math.random() * (b-a); 19 function terrain(x) { 20 for (let i=1;i<ridge.length;i++) if (x <= ridge[i][0]) { 21 const a=ridge[i-1],b=ridge[i];return a[1]+(b[1]-a[1])*(x-a[0])/(b[0]-a[0]); 22 } 23 return 580; 24 } 25 const smoke = document.createElement("canvas"); smoke.width=64;smoke.height=64; 26 const sc=smoke.getContext("2d"); 27 const gradient=sc.createRadialGradient(32,32,1,32,32,31); 28 gradient.addColorStop(0,"rgba(245,247,240,.66)");gradient.addColorStop(.35,"rgba(221,234,228,.4)");gradient.addColorStop(1,"rgba(217,231,226,0)"); 29 sc.fillStyle=gradient;sc.fillRect(0,0,64,64); 30 31 function spawn(kind, x, elapsed=0, size=rand(.55,1.05)) { 32 const landing=kind==="landing"; 33 const ground=terrain(x)+8-(1-size)*12; 34 const start=landing?{x:x+rand(70,150),y:rand(-90,40)}:{x,y:ground}; 35 const end=landing?{x,y:ground-3}:{x:x+rand(85,170),y:-35}; 36 const plume={size,points:[]};plumes.push(plume); 37 flights.push({ kind, start, end, ground, size, plume, born:time-elapsed, duration:landing?rand(19500,25000):rand(18500,27000), lastEmit:-1000, entry:false }); 38 flightCount++; 39 } 40 function position(f,p) { 41 const t=f.kind==="landing"?1-Math.pow(1-p,2.45):Math.pow(p,1.4); 42 const a=f.start,b=f.end; 43 if (f.kind==="landing") { 44 const curve=Math.sin(Math.PI*t)*46; 45 return {x:a.x+(b.x-a.x)*t+curve,y:a.y+(b.y-a.y)*t}; 46 } 47 const curve=Math.pow(Math.max(0,(t-.53)/.47),1.9); 48 return {x:a.x+(b.x-a.x)*curve,y:a.y+(b.y-a.y)*t}; 49 } 50 function particle(x,y,size,vx,vy,life,opacity=.35) { 51 if(particles.length>=720)particles.shift(); 52 particles.push({x,y,size,vx,vy,life,opacity,born:time,twist:rand(0,Math.PI*2)}); 53 } 54 function entry(f,p) { 55 f.entry=true;rings.push({x:p.x,y:p.y,born:time,size:f.size}); 56 for(let i=0;i<42;i++) { 57 const a=i/42*Math.PI*2; 58 particle(p.x+Math.cos(a)*7,p.y+Math.sin(a)*3,rand(3,7)*f.size,Math.cos(a)*rand(18,35),Math.sin(a)*rand(5,15),rand(3500,6500),.42); 59 } 60 for(const puff of particles){const d=Math.hypot(puff.x-p.x,puff.y-p.y);if(d<90&&d>2){puff.vx+=(puff.x-p.x)/d*15;puff.vy+=(puff.y-p.y)/d*8;}} 61 window.dispatchEvent(new CustomEvent("scalz:atmosphere-entry",{detail:{x:p.x/1536,y:p.y/1024}})); 62 } 63 function glow(x,y,size,intensity) { 64 const radius=9*size*intensity; 65 const g=ctx.createRadialGradient(x,y,0,x,y,radius); 66 g.addColorStop(0,"rgba(255,255,233,.95)");g.addColorStop(.13,"rgba(255,239,182,.82)");g.addColorStop(.4,"rgba(255,174,92,.3)");g.addColorStop(1,"rgba(255,148,70,0)"); 67 ctx.fillStyle=g;ctx.beginPath();ctx.arc(x,y,radius,0,Math.PI*2);ctx.fill(); 68 } 69 function paint(dt) { 70 ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,canvas.width,canvas.height); 71 ctx.setTransform(ratio*worldScale,0,0,ratio*worldScale,offsetX*ratio,offsetY*ratio); 72 ctx.save();ctx.beginPath();ctx.moveTo(0,-500);ctx.lineTo(1536,-500); 73 [...ridge].reverse().forEach(p=>ctx.lineTo(p[0],p[1]));ctx.closePath();ctx.clip(); 74 // Soft overlapping volumes join the entire exhaust path; no sharp center line. 75 ctx.lineCap='round';ctx.lineJoin='round'; 76 for(let k=plumes.length-1;k>=0;k--){ 77 const plume=plumes[k];plume.points=plume.points.filter(p=>time-p.born<34000); 78 if(!plume.points.length){if(!flights.some(f=>f.plume===plume))plumes.splice(k,1);continue;} 79 const points=plume.points; 80 for(let i=0;i<points.length-1;i+=12){ 81 const segment=points.slice(i,Math.min(points.length,i+14)); 82 const age=(time-segment[0].born)/34000,fade=Math.pow(1-age,1.7),spread=(8+age*30)*plume.size; 83 for(let pass=0;pass<2;pass++){ 84 ctx.strokeStyle=pass?`rgba(242,246,237,${.21*fade})`:`rgba(154,181,178,${.12*fade})`; 85 ctx.lineWidth=spread*(pass?1:1.8);ctx.filter=`blur(${pass?2.3:3.8}px)`;ctx.beginPath(); 86 segment.forEach((p,j)=>{const seconds=(time-p.born)/1000;const x=p.x+seconds*.36+Math.sin(seconds*.42+p.y*.035)*seconds*.18,y=p.y-seconds*.2;if(j===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});ctx.stroke(); 87 } 88 } 89 } 90 ctx.filter='none'; 91 for(let i=particles.length-1;i>=0;i--) { 92 const p=particles[i],age=(time-p.born)/p.life; 93 if(age>=1){particles.splice(i,1);continue;} 94 p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=.998;p.vy*=.998; 95 const radius=p.size*(1+age*4.5); 96 ctx.globalAlpha=p.opacity*Math.pow(1-age,1.25)*Math.min(1,age*12+.22); 97 ctx.drawImage(smoke,p.x-radius,p.y-radius,radius*2,radius*2); 98 } 99 ctx.globalAlpha=1; 100 for(let i=rings.length-1;i>=0;i--) { 101 const ring=rings[i],age=(time-ring.born)/3200; 102 if(age>=1){rings.splice(i,1);continue;} 103 const radius=(12+age*150)*ring.size; 104 ctx.strokeStyle=`rgba(232,250,244,${(1-age)*.27})`;ctx.lineWidth=2.4*(1-age); 105 ctx.beginPath();ctx.ellipse(ring.x,ring.y,radius,radius*.33,-.12,0,Math.PI*2);ctx.stroke(); 106 } 107 for(let i=flights.length-1;i>=0;i--) { 108 const f=flights[i],p=Math.min(1,(time-f.born)/f.duration),point=position(f,p); 109 if(p>=1){flights.splice(i,1);continue;} 110 const landing=f.kind==="landing"; 111 if(landing&&!f.entry&&point.y>165)entry(f,point);
112 const altitude=Math.max(0,f.ground-point.y); 113 const braking=landing?Math.max(0,1-altitude/130):0; 114 const pulse=.65+.35*Math.pow(Math.sin(time*.009),2); 115 const intensity=landing?1+braking*1.9*pulse:1.05+Math.min(.35,p); 116 if(time-f.lastEmit>42) { 117 const old=position(f,Math.max(0,p-.006)); 118 const dx=point.x-old.x,dy=point.y-old.y,d=Math.hypot(dx,dy)||1; 119 f.plume.points.push({x:point.x,y:point.y,born:time}); 120 particle(point.x+rand(-1,1),point.y+rand(-1,1),rand(5,8)*f.size,rand(-.8,.8)-dx/d*.7,rand(-.7,.7)-dy/d*.7,rand(24000,34000),.25*f.size); 121 if(braking>.2)particle(point.x,point.y+4,rand(2.5,4.5)*f.size,rand(-10,10),rand(11,24),rand(6500,11000),.5*braking); 122 f.lastEmit=time; 123 } 124 glow(point.x,point.y,f.size,intensity); 125 } 126 ctx.restore();canvas.dataset.activeFlights=String(flights.length);canvas.dataset.particles=String(particles.length);canvas.dataset.plumes=String(plumes.length);canvas.dataset.launchZone='open-valley-690-855'; 127 } 128 function frame(now) { 129 if(!enabled||!visible||document.hidden||reduced.matches){raf=0;previous=0;return;} 130 const dt=previous?Math.min(100,now-previous):0;time+=dt;previous=now; 131 if(time>nextFlight&&flights.length<3){spawn(flightCount%2?"landing":"launch",rand(690,855));nextFlight=time+rand(8200,12500);} 132 if(now-painted>32){paint(Math.min(100,now-painted)/1000);painted=now;} 133 raf=requestAnimationFrame(frame); 134 } 135 function resize() { 136 const r=hero.getBoundingClientRect();width=r.width;height=r.height;ratio=Math.min(devicePixelRatio||1,1.25,1800/Math.max(width,height)); 137 canvas.width=Math.round(width*ratio);canvas.height=Math.round(height*ratio);worldScale=Math.max(width/1536,height/1024);offsetX=(width-1536*worldScale)/2;offsetY=(height-1024*worldScale)/2; 138 } 139 function sync(){if(raf)cancelAnimationFrame(raf);raf=0;previous=0;if(enabled&&visible&&!document.hidden&&!reduced.matches)raf=requestAnimationFrame(frame);} 140 spawn("launch",760,0,.85);spawn("landing",835,0,.55);nextFlight=11000; 141 new ResizeObserver(resize).observe(hero); 142 new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;sync();},{threshold:.03}).observe(hero); 143 window.addEventListener("scalz:preview-effects",e=>{enabled=Boolean(e.detail.enabled);sync();}); 144 document.addEventListener("visibilitychange",sync); 145 reduced.addEventListener("change",()=>{enabled=!reduced.matches&&!document.documentElement.classList.contains("preview-effects-off");sync();}); 146 resize();sync(); 147})();
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.