1// ============================================================ 2// LAGRANGE earthmap â å°çé½å¸ãããï¼é½å¸éãã©ã³ã¸ããï¼ 3// ã»3Då°çå â ã¡ã«ã«ãã«å³æ³ ãã·ã¼ã ã¬ã¹ã«ã¢ã¼ãåæ¿ 4// ï¼çã¡ãã·ã¥ã®åé ç¹ã«å¹³é¢åº§æ¨ aPlane ãæãããã·ã§ã¼ãã¼ã§ mixï¼ 5// ã»é½å¸ã¯SVGã¢ã¤ã³ã³ã®ãã¼ã«ã¼ãã¯ãªãã¯ã§å³ããã«ï¼PC=å³1/3ï¼ã« 6// çµ±è¨ã»åç£ã»æ¦è¦ã表示 â ãç§»åãããã§èªè·¯ã¢ãã¡ â ç¾å°ã®é½å¸ãã¥ã¼ã¸ 7// ã»åºçºç¹ã¯ç¨®åå³¶ï¼çé¸å°ï¼ã以å¾ã¯ã©ã®é½å¸ããã§ãéãã 8// æ¤è¨¼å£: window.__emap = { open, view, select, travel, render } 9// ============================================================ 10import * as THREE from 'three'; 11import { on, emit } from './core.js?b=1786340351'; 12import { CITIES, TANEGASHIMA, gotoCity } from './earth_cities.js?b=1786340351'; 13 14// é½å¸ã®å°çã»çµ±è¨ã»ã¢ã¤ã³ã³ç¨®ï¼CITIES ã¨åé ã»æ«å°¾=種åå³¶ï¼ 15const GEO = [ 16 { ll: [35.68, 139.77], icon: 'tower', pop: '790ä¸', mac: '118䏿©', ind: 'è¡æ¿ã»é»è³ã»è¦³å ', mei: 'æç¯ãããµã¤ã³ï¼å¯¿å¸' }, 17 { ll: [34.69, 135.50], icon: 'bridge', pop: '380ä¸', mac: '52䏿©', ind: '忥ã»é£å', mei: 'éé å çæ¿ï¼ããç¼ã' }, 18 { ll: [35.18, 136.91], icon: 'kiln', pop: '210ä¸', mac: '340䏿©', ind: '窯æ¥ã»æ©æ¢°', mei: '常æ»ç¼ï¼å «ä¸å³å' }, 19 { ll: [31.23, 121.47], icon: 'pearl', pop: '1,150ä¸', mac: '160䏿©', ind: '交æã»éè', mei: 'è¶èï¼å¤çã®å¤æ¯' }, 20 { ll: [48.86, 2.35], icon: 'eiffel', pop: '460ä¸', mac: '38䏿©', ind: 'è¸è¡ã»è¦³å ', mei: 'ãã²ããï¼ä¼¼é¡çµµ' }, 21 { ll: [40.71, -74.01], icon: 'skyline', pop: '870ä¸', mac: '85䏿©', ind: 'éèã»æå', mei: 'ãããããã°ï¼å°ä¸é' }, 22 { ll: [34.05, -118.24], icon: 'palm', pop: '640ä¸', mac: '64䏿©', ind: 'æ åã»æ¸¯æ¹¾', mei: 'ã¿ã³ã¹ï¼å¤é½' }, 23 { ll: [-22.91, -43.17], icon: 'statue', pop: '580ä¸', mac: '40䏿©', ind: '観å ã»æ¼æ¥', mei: 'ã«ã¼ããã«ï¼é®®é' }, 24 { ll: [28.61, 77.21], icon: 'gate', pop: '1,380ä¸', mac: '120䏿©', ind: 'è±åã»äº¤æ', mei: 'ããªã¼ã´ã¼ã«ãï¼ãã£ã¤' }, 25 { ll: [1.35, 103.82], icon: 'tree', pop: '330ä¸', mac: '45䏿©', ind: 'ä¸ç¶è²¿æã»åè¸', mei: 'ã©ã¯ãµï¼ã¹ã¼ãã¼ããªã¼' }, 26 { ll: [55.76, 37.62], icon: 'dome', pop: '560ä¸', mac: '52䏿©', ind: 'å¦è¡ã»æ¸åº«', mei: 'ç´ã®æ¬ï¼ã¸ã£ã ç´ è¶' }, 27 { ll: [24.71, 46.68], icon: 'palmtower', pop: '390ä¸', mac: '33䏿©', ind: '天æã»è¾²å', mei: 'ãã¼ãï¼å¤ã®æè¦' }, 28 { ll: [41.01, 28.98], icon: 'mosque', pop: '740ä¸', mac: '58䏿©', ind: '海峡交æ', mei: 'é¦è¾æï¼ã·ããã' }, 29 { ll: [41.90, 12.50], icon: 'colosseum', pop: '260ä¸', mac: '21䏿©', ind: 'éºè·¡ã»é£', mei: 'ã«ã«ããã¼ã©ï¼ä¸ã®ã¯ã¤ã³' }, 30 { ll: [30.37, 130.96], icon: 'rocket', pop: '3.8ä¸', mac: '1.2䏿©', ind: 'å®å®æ¸¯ã»æ¼æ¥', mei: 'å®ç´èï¼çºå°å°è·¡å·¡ç¤¼' }, 31]; 32// SVGã©ã¤ã³ã¢ã¤ã³ã³ï¼24x24ã»strokeæç»ï¼ 33const ICONS = { 34 rocket: '<path d="M12 3c2.6 2 3.4 6 3.4 9l-3.4 4-3.4-4c0-3 .8-7 3.4-9z"/><path d="M8.6 12l-2.6 4h4M15.4 12l2.6 4h-4M12 16v4"/>', 35 tower: '<path d="M12 3l4.5 15h-9L12 3z"/><path d="M8.8 13h6.4M12 18v3"/>', 36 bridge: '<path d="M3 15c3-5 15-5 18 0"/><path d="M5 15v4M12 12v7M19 15v4"/>', 37 kiln: '<path d="M6 19c0-6 2.5-10 6-12 3.5 2 6 6 6 12z"/><path d="M9 19c0-4 1.2-7 3-8.6 1.8 1.6 3 4.6 3 8.6"/>', 38 pearl: '<path d="M12 2v5M12 12v9"/><circle cx="12" cy="9" r="2.4"/><circle cx="12" cy="15" r="1.5"/>', 39 eiffel: '<path d="M12 3c1 5 3.5 12 7 17M12 3c-1 5-3.5 12-7 17"/><path d="M8 14h8M9.5 9.5h5"/>', 40 skyline: '<path d="M5 20V9h4v11M11 20V4h4v16M17 20v-8h3v8"/>', 41 palm: '<path d="M12 9c0 4-.5 8-1.5 11"/><path d="M12 9C10 6 7 5.5 4.5 7c2.5.5 5 1 7.5 2zM12 9c2-3 5-3.5 7.5-2-2.5.5-5 1-7.5 2zM12 9c0-3 2-5 4-5.5-1.5 1.5-2.5 3.5-2.7 5"/>', 42 statue: '<path d="M12 6v9M6 8l6 3 6-3"/><circle cx="12" cy="4.4" r="1.6"/><path d="M9 20h6l-1-5h-4z"/>', 43 gate: '<path d="M5 20V9a7 7 0 0 1 14 0v11"/><path d="M9 20v-7a3 3 0 0 1 6 0v7M3.5 20h17"/>', 44 tree: '<path d="M12 21V9"/><path d="M12 9c-4 0-6-2.5-6-5 3 0 5.5 1 6 4 .5-3 3-4 6-4 0 2.5-2 5-6 5z"/><path d="M8.5 13c1 .8 2.3 1.3 3.5 1.3s2.5-.5 3.5-1.3"/>', 45 dome: '<path d="M12 3c1.8 2 3 4.5 3 7H9c0-2.5 1.2-5 3-7z"/><path d="M8 10h8v4H8zM7 20v-6M17 20v-6M7 20h10"/>', 46 palmtower: '<path d="M10 21c.6-4 .6-9 0-13"/><path d="M10 8C8.5 6 6.5 5.6 5 6.6c1.7.4 3.4.8 5 1.4zM10 8c1.5-2 3.5-2.4 5-1.4-1.7.4-3.4.8-5 1.4z"/><path d="M17 12a4.5 4.5 0 1 0 3 7.9 4 4 0 1 1-3-7.9z"/>', 47 mosque: '<path d="M12 4c2.5 2 4 4 4 6H8c0-2 1.5-4 4-6z"/><path d="M6 20V10M18 20v-10M4.5 20h15M6 10V7M18 10V7"/>', 48 colosseum: '<path d="M4 10c2.5-1.6 13.5-1.6 16 0v9H4z"/><path d="M7 19v-5M11 19v-5M15 19v-5M19 19v-5M4 14h16"/>', 49}; 50const iconSvg = (k, sz, cls) => `<svg class="${cls || ''}" width="${sz}" height="${sz}
50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${ICONS[k] || ICONS.tower}</svg>`; 51 52const MK = Math.log(Math.tan(Math.PI / 4 + (78 * Math.PI / 180) / 2)); // ã¡ã«ã«ãã«yæå¤§ï¼Â±78°ã¯ã©ã³ã=極ã®é伸é·ãæããï¼ 53const PW = 3.3; // å¹³é¢ã®å¹ 54const PH = PW * (MK / Math.PI); // å¹³é¢ã®é«ãï¼â2.13ã»å®ç¨ä¸çå°å³ã®æ¨ªé·æ¯ï¼ 55 56let built = false, isOpen = false, view3d = true; // view3d=false â ã¡ã«ã«ãã« 57let curIdx = TANEGASHIMA, selIdx = -1, traveling = false; 58let renderer = null, scene = null, camera = null, mesh = null, mat = null, routeLine = null; 59let dragging = false, dragX = 0, dragY = 0, autoRot = true, zoomJp = false; 60const $id = id => document.getElementById(id); 61 62// ---------- 座æ¨ç³»ï¼ã·ã§ã¼ãã¼ã¨åä¸ã®å¼ãJSå´ã§ã使ãï¼ ---------- 63function sphPos(lat, lon, out) { 64 // three.SphereGeometry ã¨åãåå: u=(lon+180)/360, phi=u*2Ï 65 const phi = ((lon + 180) / 360) * Math.PI * 2; 66 const th = (90 - lat) * Math.PI / 180; 67 out.set(-Math.cos(phi) * Math.sin(th), Math.cos(th), Math.sin(phi) * Math.sin(th)); 68 return out; 69} 70function plnPos(lat, lon, out) { 71 const la = Math.max(-78, Math.min(78, lat)) * Math.PI / 180; 72 out.set(((lon + 180) / 360 - 0.5) * PW, Math.log(Math.tan(Math.PI / 4 + la / 2)) / MK * (PH / 2), 0); 73 return out; 74} 75const _s = new THREE.Vector3(), _p = new THREE.Vector3(), _w = new THREE.Vector3(); 76function morphPos(lat, lon, m, out) { 77 sphPos(lat, lon, _s); plnPos(lat, lon, _p); 78 // çå´ã ãã¡ãã·ã¥ã®å転ï¼ã¨ã¼+ãããï¼ãåãããå¹³é¢å´ã¯æ£é¢åºå® 79 //ï¼ã¢ã¼ãä¸ã¯å転â0ã並走ãããã®ã§ç¹ãç®ã¯é£ç¶ï¼ 80 if (mesh) _s.applyEuler(mesh.rotation); 81 out.lerpVectors(_s, _p, m); 82 return out; 83} 84 85// ---------- DOM ---------- 86function build() { 87 if (built) return; built = true; 88 const css = document.createElement('style'); 89 css.textContent = ` 90#emOv{position:fixed;inset:0;z-index:1255;display:none;background:rgba(3,7,9,.96);backdrop-filter:blur(6px)} 91#emOv.on{display:flex;flex-direction:column} 92#emHead{display:flex;align-items:center;gap:14px;padding:10px 18px;border-bottom:1px solid rgba(121,231,221,.25);background:rgba(6,10,12,.9)} 93#emHead b{color:#ffe9a8;font-size:13px;letter-spacing:.22em} 94#emCur{color:#79e7dd;font-size:11px;letter-spacing:.08em} 95#emViewSw{display:flex;margin-left:auto;border:1px solid rgba(121,231,221,.35)} 96#emViewSw button{background:none;border:none;color:#9fc9c9;padding:7px 16px;font-size:11px;letter-spacing:.14em;cursor:pointer;font-family:var(--maru)} 97#emViewSw button.on{background:rgba(121,231,221,.16);color:#dffdf9} 98#emClose{background:none;border:1px solid rgba(232,185,77,.5);color:#ffe9a8;padding:7px 18px;cursor:pointer;font-size:11px;letter-spacing:.14em} 99#emClose:hover{background:rgba(232,185,77,.15)} 100#emBody{flex:1;display:flex;min-height:0} 101#emMapWrap{position:relative;flex:1;min-width:0;overflow:hidden} 102#emMapWrap canvas{display:block;width:100%;height:100%;cursor:grab} 103#emMapWrap.drag canvas{cursor:grabbing} 104#emMarks{position:absolute;inset:0;pointer-events:none;overflow:hidden} 105.emMk{position:absolute;transform:translate(-50%,-100%);pointer-events:auto;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:2px;color:#8fe8de;transition:color .2s} 106.emMk:hover{color:#dffdf9} 107.emMk .mkIc{background:rgba(5,12,14,.82);border:1px solid rgba(121,231,221,.45);padding:4px;display:grid;place-items:center} 108.emMk small{font-size:10px;letter-spacing:.1em;background:rgba(5,12,14,.75);padding:1px 6px;white-space:nowrap;font-family:var(--maru)} 109.emMk.cur{color:#ffd98a}.emMk.cur .mkIc{border-color:rgba(232,185,77,.7)} 110.emMk.sel{color:#fff}.emMk.sel .mkIc{border-color:#79e7dd;box-shadow:0 0 14px rgba(121,231,221,.5)} 111.emMk.dim{opacity:0;pointer-events:none} 112#emShuttle{position:absolute;pointer-events:none;color:#ffe9a8;display:none;transform:translate(-50%,-50%)} 113#emTravelTag{position:absolute;
113left:50%;top:14px;transform:translateX(-50%);display:none;color:#dffdf9;font-family:var(--mono);font-size:11px;letter-spacing:.24em;background:rgba(5,12,14,.8);border:1px solid rgba(121,231,221,.4);border-left:3px solid #e8b94d;padding:6px 16px} 114#emInfo{width:min(33vw,390px);flex:none;border-left:1px solid rgba(121,231,221,.22);background:rgba(7,11,13,.92);overflow-y:auto;display:flex;flex-direction:column} 115#emInfo .noSel{margin:auto;color:#5d8f8f;font-size:12px;letter-spacing:.1em;text-align:center;line-height:2} 116#emInfoBody{display:none;flex-direction:column} 117#emInfoBody.on{display:flex} 118#emInfoBody img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block} 119.emIhead{display:flex;align-items:center;gap:10px;padding:12px 16px 4px;color:#ffe9a8} 120.emIhead h3{font-size:16px;letter-spacing:.14em;font-family:var(--maru)} 121.emIhead small{color:#79e7dd;font-size:10px;letter-spacing:.2em} 122.emDesc{padding:8px 16px;color:#b8d4d2;font-size:12px;line-height:1.9;font-family:var(--maru)} 123.emStats{margin:6px 16px;border-top:1px solid rgba(121,231,221,.2)} 124.emStats div{display:flex;gap:10px;padding:7px 2px;border-bottom:1px solid rgba(121,231,221,.12);font-size:12px} 125.emStats b{flex:none;width:64px;color:#79e7dd;font-weight:400;letter-spacing:.12em;font-size:11px} 126.emStats span{color:#e6f2f0;font-family:var(--maru)} 127.emPeople{padding:4px 16px 8px;color:#9fbcbc;font-size:11.5px;line-height:1.8;font-family:var(--maru)} 128#emBtns{display:flex;gap:10px;padding:10px 16px 18px;margin-top:auto} 129#emGoBtn{flex:1;background:rgba(232,185,77,.14);border:1px solid rgba(232,185,77,.6);color:#ffe9a8;padding:12px;cursor:pointer;font-size:13px;letter-spacing:.2em;font-family:var(--maru);font-weight:700} 130#emGoBtn:hover{background:rgba(232,185,77,.26)} 131#emGoBtn[disabled]{opacity:.4;cursor:default} 132#emHereTag{flex:1;display:grid;place-items:center;border:1px dashed rgba(121,231,221,.4);color:#79e7dd;font-size:12px;letter-spacing:.2em} 133@media (max-width:900px){ 134 #emBody{flex-direction:column} 135 #emInfo{width:100%;height:44vh;border-left:none;border-top:1px solid rgba(121,231,221,.25)} 136 #emViewSw button{padding:7px 10px} 137}`; 138 document.head.appendChild(css); 139 const el = document.createElement('div'); 140 el.id = 'emOv'; 141 el.innerHTML = ` 142 <div id="emHead"><b>EARTH MAP â å°çé½å¸ããã</b><span id="emCur"></span> 143 <div id="emViewSw"><button id="emV3d" class="on">3Dçä½</button><button id="emVmap">ã¡ã«ã«ãã«</button><button id="emVjp">æ¥æ¬æ¡å¤§</button></div> 144 <button id="emClose">éãã [ESC]</button></div> 145 <div id="emBody"> 146 <div id="emMapWrap"><div id="emMarks"></div> 147 <div id="emShuttle">${iconSvg('rocket', 22)}</div> 148 <div id="emTravelTag"></div> 149 </div> 150 <aside id="emInfo"> 151 <div class="noSel">é½å¸ãã¼ã«ã¼ã鏿ãã¦ãã ãã<br>MERCATOR / GLOBE åæ¿å¯</div> 152 <div id="emInfoBody"> 153 <img id="emImg" alt=""> 154 <div class="emIhead"><span id="emIcon"></span><div><h3 id="emName"></h3><small id="emEn"></small></div></div> 155 <div class="emDesc" id="emDesc"></div> 156 <div class="emStats"> 157 <div><b>人å£</b><span id="emPop"></span></div><div><b>æ©æ¢°</b><span id="emMac"></span></div> 158 <div><b>ä¸»ç£æ¥</b><span id="emInd"></span></div> 159 <div><b>åç£</b><span id="emMei"></span></div> 160 </div> 161 <div class="emPeople" id="emPeople"></div> 162 <div id="emBtns"></div> 163 </div> 164 </aside> 165 </div>`; 166 document.body.appendChild(el); 167 $id('emClose').onclick = close; 168 $id('emV3d').onclick = () => setView(true); 169 $id('emVmap').onclick = () => setView(false); 170 $id('emVjp').onclick = () => zoomJapan(); 171 addEventListener('keydown', e => { if (e.code === 'Escape' && isOpen && !traveling) close(); }); 172 buildScene(); 173 buildMarkers(); 174 addEventListener('resize', onResize); 175} 176 177// ---------- three.js ã·ã¼ã³ï¼çâå¹³é¢ã¢ã¼ãï¼ ---------- 178function buildScene() { 179 const wrap = $id('emMapWrap'); 180 renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 181 renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); 182 wrap.insertBefore(renderer.domElement, wrap.firstChild); 183 scene = new THREE.Scene(); 184 camera = new THREE.PerspectiveCamera(45, 1, .1, 20); 185 camera.position.set(0, 0, 3.6); 186 const geo = new THREE.SphereGeometry(1, 128, 80); 187 // åé ç¹ã®å¹³é¢ï¼ã¡ã«ã«ãã«ï¼ã¿ã¼ã²ãã座æ¨ã uv ããç¼ãè¾¼ã 188 const uv = geo.attributes.uv, n = uv.count; 189 const aPlane = new Float32Array(n * 3); 190 for (let i = 0; i < n; i++) { 191 const u = uv.getX(i), v = uv.getY(i); 192 const lat = (v - 0.5) * 180, la = Math.max(-78, Math.min(78, lat)) * Math.PI / 180; 193 aPlane[i * 3] = (u - 0.5) * PW; 194 aPlane[i * 3 + 1] = Math.log(Math.tan(Math.PI / 4 + la / 2)) / MK * (PH / 2); 195 aPlane[i * 3 + 2] = 0; 196 } 197 geo.setAttribute('aPlane', new THREE.BufferAttribute(aPlane, 3)); 198 // 注: ShaderMaterialã¯è²ç©ºéã®èªååã¨ã³ã³ã¼ããç¡ãã®ã§ãããã¦ãã³ã¼ãç¡ã(ãªãã¢æ±ã)ã§ 199 // JPEGã®è¦ãç®ã®è²ããã®ã¾ã¾ä½¿ãï¼SRGBæå®ããã¨æµ·ãé»ãæ²ãï¼ 200 const tex = new THREE.TextureLoader().load('assets/earth_atmos_2048.jpg'); 201 mat = new THREE.ShaderMaterial({ 202 uniforms: { uM: { value: 0 }, map: { value: tex }, uLight: { value: new THREE.Vector3(.5, .4, 1).normalize() } }, 203 vertexShader: ` 204 attribute vec3 aPlane; uniform float uM; varying vec2 vUv; varying vec3 vN; 205 void main(){ 206 vUv = uv; 207 vec3 p = mix(position, aPlane, uM); 208 vN = normalize(normalMatrix * normalize(mix(normalize(position), vec3(0.,0.,1.), uM))); 209 gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.); 210 }`, 211 fragmentShader: ` 212 uniform sampler2D map; uniform vec3 uLight; varying vec2 vUv; varying vec3 vN; 213 void main(){ 214 vec3 c = texture2D(map, vUv).rgb; 215 float li = .62 + .48 * max(dot(normalize(vN), normalize(uLight)), 0.); 216 gl_FragColor = vec4(c * li, 1.); 217 }`, 218 }); 219 mesh = new THREE.Mesh(geo, mat); 220 scene.add(mesh); 221 // èªè·¯ã©ã¤ã³ï¼ç§»åã¢ãã¡ç¨ã»64ç¹ï¼ 222 const lg = new THREE.BufferGeometry(); 223 lg.setAttribute('position', new THREE.BufferAttribute(new Float32Array(65 * 3), 3)); 224 routeLine = new THREE.Line(lg, new THREE.LineBasicMaterial({ color: 0x8fe8de, transparent: true, op
224acity: 0 })); 225 routeLine.frustumCulled = false; 226 scene.add(routeLine); 227 // ãã©ãã°ã§å°çåãåãï¼çä½ãã¥ã¼ã®ã¿ï¼ 228 const cv = renderer.domElement; 229 cv.addEventListener('pointerdown', e => { if (!view3d || traveling) return; dragging = true; autoRot = false; dragX = e.clientX; dragY = e.clientY; $id('emMapWrap').classList.add('drag'); }); 230 addEventListener('pointermove', e => { 231 if (!dragging) return; 232 mesh.rotation.y += (e.clientX - dragX) * .005; // å·¦å³ 233 mesh.rotation.x = Math.max(-1.25, Math.min(1.25, mesh.rotation.x + (e.clientY - dragY) * .005)); // ä¸ä¸ 234 dragX = e.clientX; dragY = e.clientY; 235 }); 236 addEventListener('pointerup', () => { dragging = false; const w = $id('emMapWrap'); if (w) w.classList.remove('drag'); }); 237} 238function onResize() { 239 if (!isOpen || !renderer) return; 240 const wrap = $id('emMapWrap'); 241 const w = wrap.clientWidth || 800, h = wrap.clientHeight || 500; 242 renderer.setSize(w, h, false); 243 camera.aspect = w / h; 244 camera.updateProjectionMatrix(); 245} 246 247// ---------- ãã¼ã«ã¼ ---------- 248const mkEls = []; 249function buildMarkers() { 250 const box = $id('emMarks'); 251 CITIES.forEach((c, i) => { 252 const g = GEO[i]; if (!g) return; 253 const d = document.createElement('div'); 254 d.className = 'emMk'; 255 d.innerHTML = `<span class="mkIc">${iconSvg(g.icon, i === TANEGASHIMA ? 18 : 15)}</span><small>${c[0]}</small>`; 256 d.onclick = () => { select(i); emit('sfx', ['uiSel', 1]); }; 257 box.appendChild(d); 258 mkEls.push(d); 259 }); 260} 261function placeMarkers() { 262 const wrap = $id('emMapWrap'); 263 const w = wrap.clientWidth, h = wrap.clientHeight; 264 const m = mat.uniforms.uM.value; 265 mkEls.forEach((el, i) => { 266 const g = GEO[i]; 267 morphPos(g.ll[0], g.ll[1], m, _w); 268 // çä½ãã¥ã¼ã§ã¯è£å´ãé ãï¼morphãé²ãã»ã©å ¨è¡¨ç¤ºã¸ï¼ 269 const backside = _w.z < 0.08 && m < 0.55; 270 _w.multiplyScalar(1.02); 271 const v = _w.project(camera); 272 el.style.left = ((v.x * .5 + .5) * w) + 'px'; 273 el.style.top = ((-v.y * .5 + .5) * h) + 'px'; 274 el.classList.toggle('dim', backside); 275 el.classList.toggle('cur', i === curIdx); 276 el.classList.toggle('sel', i === selIdx); 277 }); 278} 279 280// ---------- ãã¥ã¼åæ¿ï¼3D â ã¡ã«ã«ãã«ï¼ ---------- 281function setView(to3d) { 282 if (traveling) return; 283 zoomJp = false; 284 gsap.to(camera.position, { z: 3.6, duration: 1.1, ease: 'power2.inOut' }); 285 if (view3d === to3d) { view3d = to3d; syncSw(); return; } 286 view3d = to3d; 287 syncSw(); 288 autoRot = false; 289 // å¹³é¢ã¸ã¯å転ã0ã¸ã»çä½ã¸æ»ãã¨ãã¯ç¾å¨å°ãæ£é¢ã« 290 const ty = to3d ? faceRot(curIdx) : nearestZero(mesh.rotation.y); 291 const tx = to3d ? faceTilt(curIdx) : 0; 292 gsap.to(mat.uniforms.uM, { value: to3d ? 0 : 1, duration: 1.25, ease: 'power2.inOut' }); 293 gsap.to(mesh.rotation, { y: ty, x: tx, duration: 1.25, ease: 'power2.inOut', onComplete: () => { autoRot = to3d; } }); 294 emit('sfx', ['mapOpen', .7]); 295} 296// æ¥æ¬æ¡å¤§ â çä½ã®ã¾ã¾æ¥æ¬(種åå³¶ã»åå¤å±ã»å¤§éªã»æ±äº¬)ã¸å¯ã 297function zoomJapan() { 298 if (traveling) return; 299 if (!view3d) { // å¹³é¢ä¸ãªãå ã«çä½ã¸æ»ã 300 view3d = true; 301 gsap.to(mat.uniforms.uM, { value: 0, duration: .9, ease: 'power2.inOut' }); 302 } 303 zoomJp = true; 304 autoRot = false; 305 syncSw(); 306 const phi = ((137 + 180) / 360) * Math.PI * 2; 307 let ry = Math.PI / 2 - phi; 308 while (ry < mesh.rotation.y - Math.PI) ry += Math.PI * 2; 309 while (ry > mesh.rotation.y + Math.PI) ry -= Math.PI * 2; 310 gsap.to(mesh.rotation, { y: ry, x: 35 * Math.PI / 180, duration: 1.2, ease: 'power2.inOut' }); 311 gsap.to(camera.position, { z: 1.52, duration: 1.2, ease: 'power2.inOut' }); 312 emit('sfx', ['mapOpen', .8]); 313} 314function syncSw() { 315 $id('emV3d').classList.toggle('on', view3d && !zoomJp); 316 $id('emVmap').classList.toggle('on', !view3d); 317 $id('emVjp').classList.toggle('on', zoomJp); 318} 319function faceRot(i) { 320 // é½å¸iãæ£é¢(z+)ã«åããå転ï¼ç¾å転ããæççµè·¯ï¼ 321 const lon = GEO[i].ll[1]; 322 const phi = ((lon + 180) / 360) * Math.PI * 2; 323 let r = Math.PI / 2 - phi; 324 const cur = mesh.rotation.y; 325 while (r < cur - Math.PI) r += Math.PI * 2; 326 while (r > cur + Math.PI) r -= Math.PI * 2; 327 return r; 328} 329function nearestZero(r) { return Math.round(r / (Math.PI * 2)) * Math.PI * 2; } // æå¯ãã®ä¸å¨ã¼ãç¹ï¼å¹³é¢å±éæã®ä½è¨ãªå転ãé²ãï¼ 330function faceTilt(i) { return Math.max(-1.1, Math.min(1.1, GEO[i].ll[0] * Math.PI / 180)); } // é½å¸ã®ç·¯åº¦ãç»é¢ä¸å¤®ã¸ 331 332// ---------- é½å¸é¸æ â å³ããã« ---------- 333function select(i) { 334 selIdx = i; 335 const c = CITIES[i], g = GEO[i]; 336 $id('emInfo').querySelector('.noSel').style.display = 'none'; 337 const b = $id('emInfoBody'); 338 b.classList.add('on'); 339 $id('emImg').src = `assets/earth/city_${c[2]}.webp`; 340 $id('emIcon').innerHTML = iconSvg(g.icon, 26); 341 $id('emIcon').style.color = '#ffe9a8'; 342 $id('emName').textContent = c[0]; 343 $id('emEn').textContent = c[1]; 344 $id('emDesc').textContent = c[3]; 345 $id('emPop').textContent = g.pop; 346 $id('emMac').textContent = g.mac || 'â'; 347 $id('emInd').textContent = g.ind; 348 $id('emMei').textContent = g.mei; 349 $id('emPeople').innerHTML = 'ãä¼ãã人ã ' + c[4].map(p => `<b style="color:#dff3f3">${p[0]}</b>`).join('ã'); 350 const btns = $id('emBtns'); 351 if (i === curIdx) { 352 btns.innerHTML = '<div id="emHereTag">ç¾å¨å°</div>'; 353 } else { 354 btns.innerHTML = '<button id="emGoBtn">ç§»åãã â¸</button>'; 355 $id('emGoBtn').onclick = () => travel(i); 356 } 357 if (view3d && !traveling) { autoRot = false; gsap.to(mesh.rotation, { y: faceRot(i), x: faceTilt(i), duration: .9, ease: 'power2.inOut' }); }
357 // 鏿é½å¸ãä¸å¤®ã« 358} 359 360// ---------- ç§»åã¢ãã¡ï¼èªè·¯+ã·ã£ãã«ï¼ ---------- 361const travelState = { p: 0, from: 0, to: 0 }; 362const _a = new THREE.Vector3(), _b = new THREE.Vector3(), _q = new THREE.Vector3(); 363function pathPoint(t, out) { 364 // 大åã¹ã©ã¼ã â lat/lon ã«æ»ãã¦ç¾å¨ã®ã¢ã¼ãéã§é ç½®ï¼ä¸¡ãã¥ã¼ã§æ£ããèªè·¡ã«ãªãï¼ 365 sphPos(GEO[travelState.from].ll[0], GEO[travelState.from].ll[1], _a); 366 sphPos(GEO[travelState.to].ll[0], GEO[travelState.to].ll[1], _b); 367 const ang = _a.angleTo(_b) || .0001; 368 _q.copy(_a).multiplyScalar(Math.sin((1 - t) * ang) / Math.sin(ang)) 369 .addScaledVector(_b, Math.sin(t * ang) / Math.sin(ang)).normalize(); 370 const lat = Math.asin(Math.max(-1, Math.min(1, _q.y))) * 180 / Math.PI; 371 const lon = (Math.atan2(_q.z, -_q.x) / (Math.PI * 2)) * 360 - 180; 372 morphPos(lat, lon, mat.uniforms.uM.value, out); 373 // å¼§ã®é«ãï¼çä½ãã¥ã¼ã®ã¿è¨ãã¾ããï¼ 374 const lift = Math.sin(t * Math.PI) * .12 * (1 - mat.uniforms.uM.value); 375 out.multiplyScalar(1.015 + lift); 376 return out; 377} 378function updateRoute() { 379 const pos = routeLine.geometry.attributes.position; 380 for (let k = 0; k <= 64; k++) { 381 pathPoint(k / 64, _w); 382 pos.setXYZ(k, _w.x, _w.y, _w.z); 383 } 384 pos.needsUpdate = true; 385} 386// ---------- æä¹å¸ï¼åºçº/å°çã®ç¾å°æå»ã»æè¦ã»è·é¢ã§ãæ ããå¯è¦åããï¼ ---------- 387function buildTicket(from, dest) { 388 let tk = $id('emTicket'); 389 if (!tk) { 390 const css = document.createElement('style'); 391 css.textContent = ` 392#emTicket{position:absolute;right:16px;top:60px;z-index:6;width:min(300px,82vw);background:rgba(8,13,12,.93);border:1px solid rgba(232,185,77,.6);box-shadow:0 8px 30px rgba(0,0,0,.5);backdrop-filter:blur(6px);opacity:0;transform:translateY(-8px);transition:all .5s cubic-bezier(.2,.9,.3,1);pointer-events:none} 393#emTicket.on{opacity:1;transform:none} 394#emTicket .tkHead{display:flex;justify-content:space-between;align-items:baseline;padding:8px 12px;border-bottom:1px dashed rgba(232,185,77,.45)} 395#emTicket .tkHead b{font-family:var(--mono,monospace);font-size:10px;letter-spacing:.24em;color:#e8c37a} 396#emTicket .tkHead span{font-family:var(--mono,monospace);font-size:10px;color:#79e7dd} 397#emTicket .tkBody{padding:10px 12px;display:grid;grid-template-columns:1fr auto 1fr;gap:4px;align-items:center} 398#emTicket .tkCity{font-family:var(--maru);font-size:14.5px;color:#eafafa;font-weight:700} 399#emTicket .tkTime{font-family:var(--mono,monospace);font-size:17px;color:#ffe9a8;letter-spacing:.06em} 400#emTicket .tkSub{font-size:9.5px;color:#5d8f8f;letter-spacing:.14em;font-family:var(--mono,monospace)} 401#emTicket .tkArrow{color:#79e7dd;font-family:var(--mono,monospace);font-size:13px;padding:0 6px} 402#emTicket .tkFoot{padding:7px 12px;border-top:1px dashed rgba(232,185,77,.45);display:flex;justify-content:space-between;font-family:var(--mono,monospace);font-size:10px;color:#9fc9c5;letter-spacing:.1em}`; 403 document.head.appendChild(css); 404 tk = document.createElement('div'); 405 tk.id = 'emTicket'; 406 $id('emOv').appendChild(tk); 407 } 408 // 大åè·é¢ã¨ç¾å°æå»ï¼çµåº¦ããæå¸¯ãæ¨å®ï¼ 409 const a = GEO[from].ll, b = GEO[dest].ll, R = Math.PI / 180; 410 const ang = Math.acos(Math.min(1, Math.sin(a[0] * R) * Math.sin(b[0] * R) + Math.cos(a[0] * R) * Math.cos(b[0] * R) * Math.cos((b[1] - a[1]) * R))); 411 const km = Math.round(ang * 6371); 412 const durH = km / 1150 + 0.5; // æºè»éã©ã¤ãã¼ï¼ä¹é 413 const tzA = Math.round(a[1] / 15), tzB = Math.round(b[1] / 15); 414 const nowUtc = Date.now() + new Date().getTimezoneOffset() * 60000; 415 const fmt = ms => { const d2 = new Date(ms); return String(d2.getHours()).padStart(2, '0') + ':' + String(d2.getMinutes()).padStart(2, '0'); }; 416 const dep = fmt(nowUtc + tzA * 3600000); 417 const arr = fmt(nowUtc + tzB * 3600000 + durH * 3600000); 418 const h = Math.floor(durH), m = Math.round((durH - h) * 60); 419 tk.innerHTML = ` 420 <div class="tkHead"><b>BOARDING PASS</b><span>ET-${String(from * 20 + dest).padStart(3, '0')}</span></div>
421 <div class="tkBody"> 422 <div><div class="tkCity">${CITIES[from][0]}</div><div class="tkTime">${dep}</div><div class="tkSub">åºçºã»ç¾å°æå»</div></div> 423 <div class="tkArrow">>></div> 424 <div style="text-align:right"><div class="tkCity">${CITIES[dest][0]}</div><div class="tkTime">${arr}</div><div class="tkSub">å°çã»ç¾å°æå»</div></div> 425 </div> 426 <div class="tkFoot"><span>æè¦ ${h}æé${String(m).padStart(2, '0')}å</span><span>${km.toLocaleString()} km ï¼ æºè»é便</span></div>`; 427 setTimeout(() => tk.classList.add('on'), 30); // rAFã¯é表示ã¿ãã§çºç«ããªãããsetTimeout 428 return tk; 429} 430function travel(dest) { 431 if (traveling || dest === curIdx) return; 432 traveling = true; 433 autoRot = false; 434 travelState.from = curIdx; travelState.to = dest; travelState.p = 0; 435 const tag = $id('emTravelTag'); 436 tag.style.display = 'block'; 437 tag.textContent = `TRANSIT â ${CITIES[curIdx][0]} â ${CITIES[dest][0]}`; 438 $id('emShuttle').style.display = 'block'; 439 routeLine.material.opacity = .85;
440 emit('sfx', ['launch', .8]); 441 const tk = buildTicket(curIdx, dest); 442 // å°çã¸å¯ãï¼æ¡å¤§ï¼â 大åç§»å â å°çé½å¸ãæ£é¢ã« 443 if (camera) gsap.to(camera.position, { z: 2.15, duration: 1.2, ease: 'power2.inOut' }); 444 if (view3d && mesh) gsap.to(mesh.rotation, { y: faceRot(dest), x: faceTilt(dest), duration: 4.8, ease: 'power1.inOut' }); 445 gsap.to(travelState, { 446 p: 1, duration: 5.4, ease: 'power1.inOut', 447 onComplete: () => { 448 if (camera) gsap.to(camera.position, { z: 2.9, duration: .9, ease: 'power2.out' }); 449 setTimeout(() => tk.classList.remove('on'), 1400); 450 traveling = false; 451 routeLine.material.opacity = 0; 452 $id('emShuttle').style.display = 'none'; 453 tag.style.display = 'none'; 454 curIdx = dest; 455 $id('emCur').textContent = `ç¾å¨å°: ${CITIES[curIdx][0]}`; 456 emit('sfx', ['uiOk', 1]); 457 emit('toast', `${CITIES[dest][0]} ã«å°çãã¾ãã`); 458 gotoCity(dest); // èå¾ã®é½å¸ãã¥ã¼ãå·®ãæ¿ã 459 select(dest); // ããã«ã¯ç¾å¨å°è¡¨ç¤ºã« 460 setTimeout(close, 1900); // æä¹å¸ã¨å°çãºã¼ã ãè¦ãã¦ããéãã 461 }, 462 }); 463} 464function placeShuttle() { 465 if (!traveling) return; 466 updateRoute(); 467 const wrap = $id('emMapWrap'); 468 pathPoint(travelState.p, _w); 469 const v = _w.project(camera); 470 const sh = $id('emShuttle'); 471 const x = (v.x * .5 + .5) * wrap.clientWidth, y = (-v.y * .5 + .5) * wrap.clientHeight; 472 // é²è¡æ¹åã¸åé 473 pathPoint(Math.min(1, travelState.p + .02), _q); 474 const v2 = _q.project(camera); 475 const ang = Math.atan2((-v2.y + v.y) * wrap.clientHeight, (v2.x - v.x) * wrap.clientWidth) * 180 / Math.PI; 476 sh.style.left = x + 'px'; sh.style.top = y + 'px'; 477 sh.style.transform = `translate(-50%,-50%) rotate(${ang + 90}deg)`; 478} 479 480// ---------- ã«ã¼ã / éé ---------- 481function render() { 482 if (!renderer) return; 483 if (autoRot && view3d && !dragging && !traveling) mesh.rotation.y += .0016; 484 placeMarkers(); 485 placeShuttle(); 486 renderer.render(scene, camera); 487} 488function loop() { if (!isOpen) return; render(); requestAnimationFrame(loop); } 489 490function open(fromIdx) { 491 build(); 492 curIdx = (fromIdx == null ? curIdx : fromIdx); 493 selIdx = -1; 494 $id('emOv').classList.add('on'); 495 $id('emCur').textContent = `ç¾å¨å°: ${CITIES[curIdx][0]}`; 496 $id('emInfo').querySelector('.noSel').style.display = ''; 497 $id('emInfoBody').classList.remove('on'); 498 isOpen = true; 499 onResize(); 500 mesh.rotation.y = faceRot(curIdx); 501 mesh.rotation.x = faceTilt(curIdx); 502 camera.position.z = 3.6; zoomJp = false; 503 mat.uniforms.uM.value = view3d ? 0 : 1; 504 autoRot = view3d; 505 loop(); 506 emit('sfx', ['mapOpen', 1]); 507} 508function close() { 509 if (traveling) return; 510 isOpen = false; 511 $id('emOv').classList.remove('on'); 512 emit('sfx', ['uiSel', 1]); 513} 514 515export function initEarthMap() { 516 on('earthMapOpen', i => open(typeof i === 'number' ? i : undefined)); 517 window.__emap = { open, close, select, travel, render, setView, zoomJapan, get state() { return { cur: curIdx, sel: selIdx, view3d, zoomJp, camZ: camera ? camera.position.z : 0, rotX: mesh ? mesh.rotation.x : 0, traveling, morph: mat ? mat.uniforms.uM.value : null }; } }; 518}
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.