PageSourceSearch

https://www.addcomposites.com/media/trust-globe.js?v=20260914-shared-land-globe-5

js addcomposites.com collected 2026-10-02 05:45:51 UTC 16,401 bytes, 91 lines download raw bytes

1(()=>{
2  const canvases=[...document.querySelectorAll('[data-installation-globe],#homeInstallationGlobe')];
3  if(!canvases.length)return;
4  const hq=[60.2055,24.6559],dots=[[51.44,5.47],[42.16,-8.62],[45.62,5.09],[1.35,103.82],[48.39,-4.49],[46.81,-71.21],[45.89,-72.48],[40.76,-111.89],[-12.37,130.86],[34.68,-82.84],[35.23,-80.84],[43.23,-.89],[44.84,-.58],[41.98,2.82],[38.91,121.61],[31.3,120.59],[33.78,-84.4],[57.7,11.99],[41.16,-8.63],[41.66,-.88],[31.9,34.82],[24.45,54.38],[26.31,50.15],[52.38,9.74],[45.78,3.09],[42.73,-84.48],[45.5,-73.57],[43.86,-79.34],[45.42,-75.7],[63.43,10.4],[41.55,-8.43],[22.32,114.17],[37.77,-122.42],[40.43,-86.91],[42.73,-73.68],[51.51,-.13],[58.41,15.62],[51.99,4.33],[23.02,113.39],[50.85,4.35],[43.26,-2.94],[33.21,-87.57],[53.35,-6.26],[32.88,-117.23],[49.84,24.03],[49.26,-123.25],[52.68,-8.57],[53.38,-1.47],[32.74,-97.11],[50.77,15.05],[39.9,116.41],[17.385,78.4867]];
5  const tours=[
6    {label:'Nordics · engineering and integration',lat:58,lng:18,zoom:1.42,focus:{box:[-30,34,53,72]}},
7    {label:'Europe · dense production network',lat:47,lng:4,zoom:1.52,focus:{continent:['Europe'],box:[-12,44,34,72]}},
8    {label:'North America · research to production',lat:42,lng:-91,zoom:1.4,focus:{continent:['North America']}},
9    {label:'Asia-Pacific · growing installation base',lat:24,lng:112,zoom:1.42,focus:{continent:['Asia'],not:['Western Asia']}},
10    {label:'Middle East · advanced manufacturing',lat:26,lng:52,zoom:1.5,focus:{subregi
10on:['Western Asia']}},
11    {label:'Oceania · applied composites research',lat:-18,lng:135,zoom:1.42,focus:{continent:['Oceania']}}
12  ];
13  let features=[],centroids=[],dotFeature=[];
14  const geo=fetch('/media/globe/countries.geojson').then(r=>r.ok?r.json():Promise.reject()).then(d=>(features=d.features||[])).catch(()=>[]);
15  const reduced=matchMedia('(prefers-reduced-motion: reduce)').matches,con=navigator.connection||{},reducedData=!!(con.saveData||/(^|-)2g$/.test(con.effectiveType||''));
16  const rings=g=>{if(!g)return[];if(g.type==='Polygon')return g.coordinates;const out=[];(g.coordinates||[]).forEach(p=>p.forEach(r=>out.push(r)));return out};
17  const clamp=(n,a,b)=>Math.max(a,Math.min(b,n));
18  const wrap=n=>{while(n>Math.PI)n-=Math.PI*2;while(n< -Math.PI)n+=Math.PI*2;return n};
19  const mix=(a,b,t)=>a+(b-a)*t;
20  const inBox=(lng,lat,b)=>!!b&&lng>=b[0]&&lng<=b[1]&&lat>=b[2]&&lat<=b[3];
21  // Region focus uses the geojson's own CONTIENT/SUBREGION properties plus the tour's
22  // bounding box, so a tour lights the real countries it covers rather than a circle.
23  const matchFocus=(f,lng,lat,props)=>{
24    if(!f)return false;
25    if(f.not&&props&&f.not.includes(props.SUBREGION))return false;
26    if(f.continent&&props&&f.continent.includes(props.CONTINENT))return true;
27    if(f.subregion&&props&&f.subregion.includes(props.SUBREGION))return true;
28    return inBox(lng,lat,f.box);
29  };
30  const ensureDerived=()=>{
31    if(centroids.length===features.length&&dotFeature.length===dots.length)return;
32    centroids=features.map(f=>{const rs=rings(f.geometry);if(!rs.length||!rs[0].length)return{cx:0,cy:0};const r=rs[0];let x=0,y=0;for(const c of r){x+=c[0];y+=c[1]}return{cx:x/r.length,cy:y/r.length}});
33    dotFeature=dots.map(d=>{let best=0,score=Infinity;for(let i=0;i<centroids.length;i++){const c=centroids[i],dl=((c.cx-d[1]+540)%360)-180,s=Math.pow(c.cy-d[0],2)+Math.pow(dl*Math.cos(d[0]*Math.PI/180),2);if(s<score){score=s;best=i}}return best});
34  };
35  canvases.forEach((canvas,instance)=>{
36    const section=canvas.closest('[data-globe-section],.global-trust')||canvas.parentElement;
37    const toggle=section?.querySelector('[data-globe-toggle],.installation-toggle'),tourButton=section?.querySelector('[data-globe-tour]'),resetButton=section?.querySelector('[data-globe-reset]'),zoomIn=section?.querySelector('[data-globe-zoom-in]'),zoomOut=section?.querySelector('[data-globe-zoom-out]'),status=section?.querySelector('[data-globe-status]');
38    const ctx=canvas.getContext('2d');if(!ctx)return;
39    let running=false,near=false,last=0,yaw=-.18,pitch=-.13,zoom=1,renderZoom=1,dragging=false,moved=false,dragX=0,dragY=0,dragYaw=0,dragPitch=0,resumeTimer=0,W=0,H=0,R=0,CX=0,CY=0,selected=-1,animating=false,target=null,tourIndex=-1,tourTimer=0,lastFrame=0,autoTourStarted=false;
40    let focusAmt=[],focusTarget=[],tourAmt=0,morphing=false;
41    const dpr=Math.min(1.5,devicePixelRatio||1);
42    // Pause is intentionally page-local. Persisting it made a single manual
43    // pause silently disable the default tour on every later visit.
44    let userPaused=reduced||reducedData;
45    const project=(lat,lng)=>{const p=lat*Math.PI/180,l=lng*Math.PI/180+yaw,c=Math.cos(p),x=c*Math.sin(l),y0=-Math.sin(p),z0=c*Math.cos(l),cp=Math.cos(pitch),sp=Math.sin(pitch),y=y0*cp-z0*sp,z=y0*sp+z0*cp,rr=R*renderZoom;return[CX+rr*x,CY+rr*y,z]};
46    const setStatus=(label,state='focus')=>{if(!status)return;status.dataset.state=state;const text=status.querySelector('span');if(text)text.textContent=label};
47    const base=now=>{ctx.clearRect(0,0,W,H);let halo=ctx.createRadialGradient(CX-R*.2,CY-R*.15,R*.12,CX,CY,R*1.28);halo.addColorStop(0,'#215b91');halo.addColorStop(.55,'#092b50');halo.addColorStop(1,'rgba(2,12,24,0)');ctx.fillStyle=halo;ctx.beginPath();ctx.arc(CX,CY,R*1.2,0,Math.PI*2);ctx.fill();let g=ctx.createRadialGradient(CX-R*.3,CY-R*.32,R*.05,CX,CY,R*renderZoom);g.addColorStop(0,'#28689f');g.addColorStop(.58,'#103c69');g.addColorStop(1,'#03152b');ctx.fillStyle=g;ctx.beginPath();ctx.arc(CX,CY,R*renderZoom,0,Math.PI*2);ctx.fill();ctx.save();ctx.beginPath();ctx.arc(CX,CY,R*renderZoom,0,Math.PI*2);ctx.clip();const sweep=ctx.createLinearGradient(CX-R,CY-R,CX+R,CY+R);const phase=((now||0)/4200)%1;sweep.addColorStop(clamp(phase-.12,0,1),'rgba(72,159,226,0)');sweep.addColorStop(phase,'rgba(145,211,255,.12)');sweep.addColorStop(clamp(phase+.12,0,1),'rgba(72,159,226,0)');ctx.fillStyle=sweep;ctx.fillRect(CX-R*renderZoom,CY-R*renderZoom,R*2*renderZoom,R*2*renderZoom)};
48    // Only the front-facing runs of a ring are kept, so fills never bridge across the
49    // far side of the globe. Each run is closed on itself.
50    const runsFor=r=>{const runs=[];let cur=null;for(const c of r){const p=project(c[1],c[0]);if(p[2]>.02){if(!cur){cur=[];runs.push(cur)}cur.push(p)}else cur=null}return runs.filter(run=>run.length>2)};
51    const polygonPath=run=>{ctx.beginPath();ctx.moveTo(run[0][0],run[0][1]);for(let i=1;i<run.length;i++)ctx.lineTo(run[i][0],run[i][1]);ctx.closePath()};
52    const collectRuns=()=>features.map(f=>{const out=[];
52rings(f.geometry).forEach(r=>runsFor(r).forEach(run=>out.push(run)));return out});
53    const computeFocus=()=>{if(!features.length){focusTarget=[];return}if(tourIndex<0){focusTarget=features.map(()=>0);return}const f=tours[tourIndex].focus;focusTarget=features.map((ft,i)=>matchFocus(f,centroids[i].cx,centroids[i].cy,ft.properties)?1:0)};
54    const dotInFocus=i=>{if(tourIndex<0||!features.length)return false;const ft=features[dotFeature[i]];return matchFocus(tours[tourIndex].focus,dots[i][1],dots[i][0],ft&&ft.properties)};
55    // Polygon land fills: steel/slate by default, receding slate when a tour is running,
56    // and coral for the focused region each country is classified into.
57    const drawLand=allRuns=>{for(let i=0;i<features.length;i++){const amt=focusAmt[i]||0,t=tourAmt;const r=mix(mix(126,58,t),255,amt),g=mix(mix(152,84,t),126,amt),b=mix(mix(178,112,t),90,amt),a=mix(mix(.5,.3,t),.86,amt);ctx.fillStyle=`rgba(${r|0},${g|0},${b|0},${a.toFixed(3)})`;allRuns[i].forEach(run=>{polygonPath(run);ctx.fill()})}};
58    // Sharp pale borders after the fills; the focused region keeps an amber edge.
59    const drawBorders=allRuns=>{for(let i=0;i<features.length;i++){const amt=focusAmt[i]||0,focused=amt>.5;ctx.strokeStyle=focused?`rgba(255,205,158,${(.48+.34*amt).toFixed(3)})`:'rgba(214,236,255,.58)';ctx.lineWidth=focused?1.1:(W<520?.7:1);allRuns[i].forEach(run=>{polygonPath(run);ctx.stroke()})}};
60    const arcTo=dest=>{const a=hq[0],b=hq[1],dl=((dest[1]-b+540)%360)-180;let started=false;ctx.beginPath();for(let i=0;i<=24;i++){const t=i/24,lat=a+(dest[0]-a)*t+Math.sin(Math.PI*t)*9,lng=b+dl*t,p=project(lat,lng);if(p[2]>.03){if(!started){ctx.moveTo(p[0],p[1]);started=true}else ctx.lineTo(p[0],p[1])}else started=false}ctx.stroke()};
61    const draw=now=>{base(now);if(features.length){const allRuns=collectRuns();drawLand(allRuns);drawBorders(allRuns)}ctx.strokeStyle='rgba(92,143,194,.25)';ctx.lineWidth=.65;dots.forEach(arcTo);dots.forEach((d,i)=>{const p=project(d[0],d[1]);if(p[2]<=0)return;const focused=dotInFocus(i);const r=(2.2+1.3*p[2])*(focused?1.4:1);ctx.shadowColor=focused?'#ffb27a':'#ff6f59';ctx.shadowBlur=i===selected?15:(focused?12:8);ctx.fillStyle=i===selected?'#fff1ec':(focused?'#ffd0a8':'#ff624a');ctx.beginPath();ctx.arc(p[0],p[1],i===selected?r+1.5:r,0,Math.PI*2);ctx.fill();if(i===selected){const pulse=8+10*((now||0)%1500)/1500;ctx.shadowBlur=0;ctx.strokeStyle=`rgba(255,128,105,${.72-pulse/34})`;ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(p[0],p[1],pulse,0,Math.PI*2);ctx.stroke()}});const p=project(hq[0],hq[1]);if(p[2]>0){ctx.shadowColor='#fff';ctx.shadowBlur=12;ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(p[0],p[1],4.5,0,Math.PI*2);ctx.fill()}ctx.shadowBlur=0;ctx.restore();ctx.strokeStyle='rgba(110,181,238,.58)';ctx.lineWidth=1;ctx.beginPath();ctx.arc(CX,CY,R*renderZoom,0,Math.PI*2);ctx.stroke();ctx.strokeStyle='rgba(104,172,228,.13)';ctx.beginPath();ctx.arc(CX,CY,R*renderZoom+7,0,Math.PI*2);ctx.stroke()};
62    const resize=()=>{const b=canvas.getBoundingClientRect();W=Math.max(1,Math.round(b.width));H=Math.max(1,Math.round(b.height));canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);R=Math.min(W,H)*.42;CX=W*(W<820?.56:.68);CY=H*(W<820?.62:.52);draw(performance.now())};
63    const focus=(lat,lng,nextZoom,label,index=-1,state='focus')=>{clearTimeout(tourTimer);target={yaw:-lng*Math.PI/180,pitch:clamp(-lat*Math.PI/180,-1.15,1.15),zoom:clamp(nextZoom,.9,1.65)};selected=index;animating=true;setStatus(label,state);computeFocus();morphing=true;sync()};
64    const reset=()=>{clearTimeout(tourTimer);tourIndex=-1;selected=-1;target={yaw:-.18,pitch:-.13,zoom:1};animating=true;setStatus('Global overview','overview');computeFocus();morphing=true;if(tourButton){tourButton.textContent='Tour network';tourButton.setAttribute('aria-pressed','false')}sync()};
65    const nearestDot=(lat,lng)=>{let best=-1,score=Infinity;dots.forEach((d,i)=>{const dl=((d[1]-lng+540)%360)-180,s=Math.pow(d[0]-lat,2)+Math.pow(dl*Math.cos(lat*Math.PI/180),2);if(s<score){score=s;best=i}});return best};
66    const nextTour=()=>{tourIndex=(tourIndex+1)%tours.length;const stop=tours[tourIndex];if(tourButton){tourButton.textContent='Touring network';tourButton.setAttribute('aria-pressed','true')}focus(stop.lat,stop.lng,stop.zoom,stop.label,nearestDot(stop.lat,stop.lng),'tour');tourTimer=setTimeout(nextTour,3600)};
67    const stopTour=()=>{clearTimeout(tourTimer);if(tourIndex>=0){tourIndex=-1;computeFocus();morphing=true}if(tourButton){tourButton.textContent='Tour network';tourButton.setAttribute('aria-pressed','false')}};
68    const startDefaultTour=()=>{if(autoTourStarted||userPaused||reduced||reducedData)return;autoTourStarted=true;nextTour()};
69    const tick=t=>{if(!running&&!animating&&!morphing)return;const dt=Math.min(80,Math.max(0,t-last));if(t-lastFrame>32){if(target){yaw+=wrap(target.yaw-yaw)*.085;pitch+=(target.pitch-pitch)*.085;zoom+=(target.zoom-zoom)*.075;if(Math.abs(wrap(target.yaw-yaw))<.002&&Math.abs(target.pitch-pitch)<.002&&Math.abs(target.zoom-zoom)<.003){
69yaw=target.yaw;pitch=target.pitch;zoom=target.zoom;target=null;animating=false}}else if(running&&!dragging){yaw+=.0000065*dt;if(tourIndex<0)pitch+=((Math.sin(t/13500+instance)*.105-.08)-pitch)*.008}renderZoom=zoom*(1+(running&&tourIndex<0&&!reduced?.012*Math.sin(t/1700):0));let stillMorphing=false;const tt=tourIndex>=0?1:0;if(Math.abs(tt-tourAmt)>.004){tourAmt+=(tt-tourAmt)*.14;stillMorphing=true}else tourAmt=tt;for(let i=0;i<focusAmt.length;i++){const tv=focusTarget[i]||0,v=focusAmt[i];if(Math.abs(tv-v)>.004){focusAmt[i]=v+(tv-v)*.14;stillMorphing=true}else focusAmt[i]=tv}morphing=stillMorphing;draw(t);lastFrame=t}last=t;if(running||animating||morphing)requestAnimationFrame(tick)};
70    const updateToggle=()=>{if(!toggle)return;toggle.textContent=userPaused?'Resume rotation':'Pause rotation';toggle.setAttribute('aria-pressed',userPaused?'true':'false')};
71    const sync=()=>{const shouldRun=near&&!userPaused&&!dragging&&!document.hidden;if((shouldRun||animating||morphing)&&!running){running=shouldRun;last=performance.now();requestAnimationFrame(tick)}else{running=shouldRun;if(!running&&!animating&&!morphing)draw(performance.now())}updateToggle()};
72    const setPaused=paused=>{userPaused=paused;sync()};
73    const setZoom=value=>{stopTour();target={yaw,pitch,zoom:clamp(value,.9,1.65)};animating=true;selected=-1;setStatus(target.zoom>1.05?'Closer network view':'Global overview',target.zoom>1.05?'focus':'overview');sync()};
74    toggle?.addEventListener('click',()=>{clearTimeout(resumeTimer);const pause=!userPaused;if(pause)stopTour();setPaused(pause);if(!pause&&near&&!reduced&&!reducedData)nextTour()});
75    tourButton?.addEventListener('click',()=>{if(tourIndex>=0){stopTour();reset()}else{setPaused(false);nextTour()}});
76    resetButton?.addEventListener('click',()=>{stopTour();reset()});zoomIn?.addEventListener('click',()=>setZoom(zoom+.18));zoomOut?.addEventListener('click',()=>setZoom(zoom-.18));
77    canvas.addEventListener('pointerdown',e=>{stopTour();dragging=true;moved=false;clearTimeout(resumeTimer);dragX=e.clientX;dragY=e.clientY;dragYaw=yaw;dragPitch=pitch;canvas.classList.add('is-dragging');canvas.setPointerCapture(e.pointerId);sync()});
78    canvas.addEventListener('pointermove',e=>
78{if(!dragging)return;const dx=e.clientX-dragX,dy=e.clientY-dragY;moved=moved||Math.hypot(dx,dy)>6;yaw=dragYaw+dx/Math.max(180,R)*1.35;pitch=clamp(dragPitch+dy/Math.max(180,R)*1.15,-1.15,1.15);selected=-1;target=null;setStatus('Manual globe view','overview');draw(performance.now())});
79    const pick=(x,y)=>{let best=-1,dist=30;dots.forEach((d,i)=>{const p=project(d[0],d[1]);if(p[2]<=0)return;const dd=Math.hypot(p[0]-x,p[1]-y);if(dd<dist){best=i;dist=dd}});if(best>=0){const d=dots[best];focus(d[0],d[1],Math.max(1.48,zoom),'Inspecting anonymous installation region',best)}};
80    const endDrag=e=>{if(!dragging)return;dragging=false;canvas.classList.remove('is-dragging');if(!moved&&e){const b=canvas.getBoundingClientRect();pick(e.clientX-b.left,e.clientY-b.top)}clearTimeout(resumeTimer);resumeTimer=setTimeout(sync,1800)};canvas.addEventListener('pointerup',endDrag);canvas.addEventListener('pointercancel',()=>endDrag());
81    canvas.addEventListener('keydown',e=>{if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','+','=','-','_','Escape','Enter'].includes(e.key))return;e.preventDefault();stopTour();clearTimeout(resumeTimer);running=false;if(e.key==='Escape')reset();else if(e.key==='+'||e.key==='=')setZoom(zoom+.14);else if(e.key==='-'||e.key==='_')setZoom(zoom-.14);else if(e.key==='Enter')pick(CX,CY);else{target=null;selected=-1;if(e.key==='ArrowLeft'||e.key==='ArrowRight')yaw+=(e.key==='ArrowLeft'?-1:1)*.12;else pitch=clamp(pitch+(e.key==='ArrowUp'?-1:1)*.1,-1.15,1.15);setStatus('Manual globe view','overview');draw(performance.now())}resumeTimer=setTimeout(sync,1800)});
82    document.addEventListener('visibilitychange',sync);
83    addEventListener('pageshow',()=>{if(near&&tourIndex<0)startDefaultTour();else sync()});
84    new IntersectionObserver(([entry])=>{near=entry.isIntersecting;if(near){geo.then(()=>{ensureDerived();if(focusAmt.length!==features.length)focusAmt=features.map(()=>0);computeFocus();morphing=true;sync()});startDefaultTour()}sync()},{rootMargin:'120px 0px',threshold:0}).observe(section);
85    new ResizeObserver(resize).observe(canvas);resize();setStatus('Global overview','overview');updateToggle();
86    // A hash-restored or BFCache-restored story can place this section before the
87    // observer delivers its first entry. Establish visibility after layout so the
88    // default tour is deterministic rather than dependent on observer timing.
89    requestAnimationFrame(()=>requestAnimationFrame(()=>{const b=section.getBoundingClientRect();near=b.bottom>-120&&b.top<innerHeight+120;if(near)startDefaultTour();sync()}));
90  });
91})();

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.