|
|
| 1번째 줄: |
1번째 줄: |
| /* ========================================= | | /* ========================================= |
| COASTLINE: BLACK ICE - NationsGlobe | | COASTLINE: BLACK ICE - Nations Tab Panel |
| Real-world prototype / QGIS-ready globe viewer
| | 하단 국가 패널 대륙 탭 전환 전용 |
| ========================================= */ | | ========================================= */ |
| | (function () { |
| | 'use strict'; |
|
| |
|
| (function (mw, $) {
| | function initPanel(panel) { |
| 'use strict'; | | if (!panel || panel.getAttribute('data-nations-tabpanel-ready') === '1') return; |
| | panel.setAttribute('data-nations-tabpanel-ready', '1'); |
|
| |
|
| var THREE_URL = 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js'; | | var tabs = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]')); |
| var TOPOJSON_URL = 'https://cdn.jsdelivr.net/npm/topojson-client@3/+esm';
| | var bodies = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')); |
| var DEFAULT_WORLD_URL = 'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json';
| |
| var CELESTIAL_PLANET_TEXTURES = { | |
| mercury: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_mercury.jpg',
| |
| venus: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_venus_surface.jpg',
| |
| mars: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_mars.jpg',
| |
| jupiter: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_jupiter.jpg',
| |
| saturn: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_saturn.jpg',
| |
| uranus: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_uranus.jpg',
| |
| neptune: 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_neptune.jpg'
| |
| };
| |
|
| |
|
| var CELESTIAL_SUN_TEXTURE_URL = 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_sun.jpg';
| | function activate(continent) { |
| var CELESTIAL_MOON_TEXTURE_URL = 'https://images.weserv.nl/?w=512&url=https://www.solarsystemscope.com/textures/download/2k_moon.jpg';
| | tabs.forEach(function (tab) { |
| var SATURN_RING_TEXTURE_URL = 'https://upload.wikimedia.org/wikipedia/commons/2/29/Solarsystemscope_texture_8k_saturn_ring_alpha.png';
| | var active = tab.getAttribute('data-continent') === continent; |
| | | tab.classList.toggle('is-active', active); |
| /*
| | tab.setAttribute('aria-selected', active ? 'true' : 'false'); |
| * Decorative celestial model.
| | }); |
| * This is not a precise ephemeris. It keeps the planets on one distant
| |
| * ecliptic plane so they read as real 3D bodies instead of screen-composed PNGs.
| |
| */
| |
| var CELESTIAL_ECLIPTIC = {
| |
| depth: -22.0,
| |
| tiltYPerX: 0.018,
| |
| depthScale: 0.34
| |
| };
| |
| | |
| var CELESTIAL_PLANET_DEFS = [
| |
| {
| |
| key: 'mercury',
| |
| radius: 0.052,
| |
| orbitRadius: 4.15,
| |
| orbitAngle: 2.03,
| |
| inclination: -0.018,
| |
| opacity: 0.36,
| |
| rotationSpeed: 0.000056,
| |
| fallbackA: '#7f786d',
| |
| fallbackB: '#24211d'
| |
| },
| |
| {
| |
| key: 'venus',
| |
| radius: 0.086,
| |
| orbitRadius: 4.65,
| |
| orbitAngle: 2.96,
| |
| inclination: 0.024,
| |
| opacity: 0.36,
| |
| rotationSpeed: 0.000022,
| |
| fallbackA: '#b99765',
| |
| fallbackB: '#4f341a'
| |
| },
| |
| {
| |
| key: 'mars',
| |
| radius: 0.105,
| |
| orbitRadius: 5.25,
| |
| orbitAngle: 3.84,
| |
| inclination: 0.032,
| |
| opacity: 0.42,
| |
| rotationSpeed: 0.000040,
| |
| fallbackA: '#8f4938',
| |
| fallbackB: '#3a1712'
| |
| },
| |
| {
| |
| key: 'jupiter',
| |
| radius: 0.235,
| |
| orbitRadius: 8.20,
| |
| orbitAngle: 5.64,
| |
| inclination: 0.023,
| |
| opacity: 0.34,
| |
| rotationSpeed: 0.000024,
| |
| fallbackA: '#a98262',
| |
| fallbackB: '#4c3628'
| |
| },
| |
| {
| |
| key: 'saturn',
| |
| radius: 0.225,
| |
| orbitRadius: 9.15,
| |
| orbitAngle: 4.76,
| |
| inclination: -0.018,
| |
| opacity: 0.44,
| |
| rotationSpeed: 0.000022,
| |
| fallbackA: '#b89a70',
| |
| fallbackB: '#4d3724',
| |
| ring: true,
| |
| ringOpacity: 0.86,
| |
| ringInnerScale: 1.34,
| |
| ringOuterScale: 2.58,
| |
| ringTiltX: 1.08,
| |
| ringTiltY: 0.18,
| |
| ringTiltZ: -0.34,
| |
| ringTextureUrl: SATURN_RING_TEXTURE_URL
| |
| },
| |
| {
| |
| key: 'uranus',
| |
| radius: 0.116,
| |
| orbitRadius: 10.70,
| |
| orbitAngle: 1.43,
| |
| inclination: -0.026,
| |
| opacity: 0.29,
| |
| rotationSpeed: 0.000030,
| |
| fallbackA: '#7fb5bd',
| |
| fallbackB: '#24424b'
| |
| },
| |
| {
| |
| key: 'neptune',
| |
| radius: 0.100,
| |
| orbitRadius: 11.75,
| |
| orbitAngle: 0.66,
| |
| inclination: 0.031,
| |
| opacity: 0.28,
| |
| rotationSpeed: 0.000032,
| |
| fallbackA: '#43658f',
| |
| fallbackB: '#151d35'
| |
| }
| |
| ];
| |
| | |
| var CELESTIAL_SUN_DEF = {
| |
| radius: 0.35,
| |
| orbitRadius: 12.4,
| |
| orbitAngle: 2.72,
| |
| inclination: 0.000,
| |
| color: 0xffddb4,
| |
| coreOpacity: 0.90,
| |
| innerGlowOpacity: 0.74,
| |
| innerGlowScale: 4.9,
| |
| outerGlowOpacity: 0.24,
| |
| outerGlowScale: 8.6,
| |
| pulseA: 0.010,
| |
| pulseB: 0.014,
| |
| textureScrollSpeed: 0.000014
| |
| };
| |
| | |
| var CELESTIAL_MOON_DEF = {
| |
| radius: 0.145,
| |
| position: [1.72, 0.28, -0.62],
| |
| opacity: 0.72,
| |
| rotationSpeed: 0.000018,
| |
| fallbackA: '#8f8f8f',
| |
| fallbackB: '#343434'
| |
| };
| |
| var instances = [];
| |
| var threeModulePromise = null;
| |
| var topoModulePromise = null;
| |
| | |
| function escapeHtml(value) {
| |
| return String(value == null ? '' : value)
| |
| .replace(/&/g, '&') | |
| .replace(/</g, '<')
| |
| .replace(/>/g, '>')
| |
| .replace(/"/g, '"')
| |
| .replace(/'/g, ''');
| |
| }
| |
| | |
| function loadThree() { | |
| if (window.CLBI_NATIONS_GLOBE_THREE) {
| |
| return Promise.resolve(window.CLBI_NATIONS_GLOBE_THREE);
| |
| }
| |
| | |
| if (!threeModulePromise) {
| |
| threeModulePromise = import(THREE_URL).then(function (module) {
| |
| window.CLBI_NATIONS_GLOBE_THREE = module; | |
| return module;
| |
| });
| |
| }
| |
| | |
| return threeModulePromise;
| |
| }
| |
| | |
| function loadTopojson() {
| |
| if (window.CLBI_NATIONS_GLOBE_TOPOJSON) {
| |
| return Promise.resolve(window.CLBI_NATIONS_GLOBE_TOPOJSON);
| |
| }
| |
| | |
| if (!topoModulePromise) {
| |
| topoModulePromise = import(TOPOJSON_URL).then(function (module) {
| |
| window.CLBI_NATIONS_GLOBE_TOPOJSON = module;
| |
| return module;
| |
| });
| |
| }
| |
| | |
| return topoModulePromise;
| |
| }
| |
| | |
| function toNumber(value, fallback) {
| |
| var n = parseFloat(value);
| |
| return Number.isFinite(n) ? n : fallback;
| |
| }
| |
| | |
| function lonLatToVector3(THREE, lon, lat, radius) {
| |
| var phi = (90 - lat) * Math.PI / 180;
| |
| var theta = (lon + 180) * Math.PI / 180;
| |
| var sinPhi = Math.sin(phi);
| |
| | |
| return new THREE.Vector3(
| |
| -radius * sinPhi * Math.cos(theta),
| |
| radius * Math.cos(phi),
| |
| radius * sinPhi * Math.sin(theta)
| |
| );
| |
| }
| |
| | |
| function vectorToLonLat(vec) {
| |
| var n = vec.clone().normalize();
| |
| var lat = Math.asin(Math.max(-1, Math.min(1, n.y))) * 180 / Math.PI;
| |
| var lon = Math.atan2(n.z, -n.x) * 180 / Math.PI - 180;
| |
| | |
| while (lon < -180) lon += 360;
| |
| while (lon > 180) lon -= 360;
| |
| | |
| return {
| |
| lon: lon,
| |
| lat: lat
| |
| };
| |
| }
| |
| | |
| | |
| function ensureNationsGlobeInteractionStyle() {
| |
| if (document.getElementById('nation-globe-interaction-style')) return;
| |
| | |
| var style = document.createElement('style');
| |
| style.id = 'nation-globe-interaction-style';
| |
| style.textContent = [
| |
| '.clbi-nations-globe-window.has-nation-hover .clbi-nations-globe-stage{cursor:pointer;}',
| |
| '.clbi-nations-globe-window.is-dragging .clbi-nations-globe-stage{cursor:grabbing;}',
| |
| '.clbi-nations-globe-nation-tooltip{position:absolute;z-index:8;min-width:72px;max-width:220px;pointer-events:none;display:none;padding:4px 7px;background:rgba(5,5,5,.92);border:1px solid #000;color:#e2e2e2;font-size:11px;line-height:1.25;box-shadow:inset 0 1px 0 #333,inset 0 -1px 0 #111,0 4px 12px rgba(0,0,0,.45);white-space:nowrap;}',
| |
| '.clbi-nations-globe-nation-tooltip.is-visible{display:block;}',
| |
| '.clbi-nations-globe-nation-tooltip .nation-tooltip-name{font-weight:700;letter-spacing:.02em;}',
| |
| '.clbi-nations-globe-nation-tooltip .nation-tooltip-code{margin-left:6px;color:#888;font-size:10px;font-weight:500;}',
| |
| '.clbi-nations-globe-nation-tooltip .nation-tooltip-link{display:block;margin-top:2px;color:#a8a8a8;font-size:10px;}'
| |
| ].join('\n');
| |
| document.head.appendChild(style);
| |
| }
| |
| | |
| function getFeaturePage(feature) {
| |
| return feature && feature.properties ? String(feature.properties.page || '').trim() : '';
| |
| }
| |
| | |
| function calculateGeometryBBox(geometry) {
| |
| var bbox = [Infinity, Infinity, -Infinity, -Infinity];
| |
| | |
| function visit(coords) {
| |
| if (!Array.isArray(coords)) return;
| |
| if (coords.length >= 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') {
| |
| bbox[0] = Math.min(bbox[0], coords[0]);
| |
| bbox[1] = Math.min(bbox[1], coords[1]);
| |
| bbox[2] = Math.max(bbox[2], coords[0]);
| |
| bbox[3] = Math.max(bbox[3], coords[1]);
| |
| return;
| |
| }
| |
| coords.forEach(visit);
| |
| }
| |
| | |
| if (geometry) visit(geometry.coordinates);
| |
| if (!Number.isFinite(bbox[0])) return [-180, -90, 180, 90];
| |
| return bbox;
| |
| }
| |
| | |
| function isLonLatInsideBbox(lon, lat, bbox) {
| |
| if (!bbox || bbox.length < 4) return true;
| |
| if (lat < bbox[1] || lat > bbox[3]) return false;
| |
| if ((bbox[2] - bbox[0]) >= 300) return true;
| |
| return lon >= bbox[0] && lon <= bbox[2];
| |
| }
| |
| | |
| function unwrapLonNear(lon, refLon) {
| |
| var x = lon;
| |
| while (x - refLon > 180) x -= 360;
| |
| while (x - refLon < -180) x += 360;
| |
| return x;
| |
| }
| |
| | |
| function pointInRing(lon, lat, ring) {
| |
| var inside = false;
| |
| var i;
| |
| var j;
| |
| var xi;
| |
| var yi;
| |
| var xj;
| |
| var yj;
| |
| var intersect;
| |
| | |
| if (!Array.isArray(ring) || ring.length < 3) return false;
| |
| | |
| for (i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
| |
| if (!ring[i] || !ring[j]) continue;
| |
| xi = unwrapLonNear(Number(ring[i][0]), lon);
| |
| yi = Number(ring[i][1]);
| |
| xj = unwrapLonNear(Number(ring[j][0]), lon);
| |
| yj = Number(ring[j][1]);
| |
| if (!Number.isFinite(xi) || !Number.isFinite(yi) || !Number.isFinite(xj) || !Number.isFinite(yj)) continue;
| |
| intersect = ((yi > lat) !== (yj > lat)) && (lon < (xj - xi) * (lat - yi) / ((yj - yi) || 1e-12) + xi);
| |
| if (intersect) inside = !inside;
| |
| }
| |
| | |
| return inside;
| |
| }
| |
| | |
| function pointInPolygonCoordinates(lon, lat, polygon) {
| |
| var i;
| |
| | |
| if (!Array.isArray(polygon) || !polygon.length) return false;
| |
| if (!pointInRing(lon, lat, polygon[0])) return false;
| |
| | |
| for (i = 1; i < polygon.length; i += 1) {
| |
| if (pointInRing(lon, lat, polygon[i])) return false;
| |
| }
| |
| | |
| return true;
| |
| }
| |
| | |
| function pointInGeometry(lon, lat, geometry) {
| |
| var i;
| |
| | |
| if (!geometry) return false;
| |
| | |
| if (geometry.type === 'Polygon') {
| |
| return pointInPolygonCoordinates(lon, lat, geometry.coordinates);
| |
| }
| |
| | |
| if (geometry.type === 'MultiPolygon') {
| |
| for (i = 0; i < geometry.coordinates.length; i += 1) {
| |
| if (pointInPolygonCoordinates(lon, lat, geometry.coordinates[i])) return true;
| |
| }
| |
| }
| |
| | |
| return false;
| |
| }
| |
| | |
| function normalizeNationLinkMap(payload) {
| |
| var source = payload && payload.items ? payload.items : (payload || {});
| |
| var map = {};
| |
| | |
| Object.keys(source).forEach(function (key) {
| |
| var entry = source[key];
| |
| var page = String((entry && entry.page) || key || '').trim();
| |
| if (!page) return;
| |
| if (typeof entry === 'string') {
| |
| map[page] = {
| |
| page: page,
| |
| name_ko: entry,
| |
| wiki_title: entry,
| |
| enabled: true
| |
| };
| |
| } else {
| |
| map[page] = Object.assign({
| |
| page: page,
| |
| name_ko: '',
| |
| wiki_title: '',
| |
| enabled: true,
| |
| note: ''
| |
| }, entry || {}, { page: page });
| |
| }
| |
| });
| |
| | |
| return map;
| |
| }
| |
| | |
| function encodeWikiTitle(title) {
| |
| return String(title || '')
| |
| .trim()
| |
| .replace(/ /g, '_')
| |
| .split('/')
| |
| .map(function (part) { return encodeURIComponent(part); })
| |
| .join('/');
| |
| }
| |
| | |
| function normalizeLonDelta(a, b) {
| |
| var d = b - a;
| |
| if (d > 180) d -= 360;
| |
| if (d < -180) d += 360;
| |
| return d;
| |
| }
| |
| | |
| function pushLineSegment(THREE, positions, a, b, radius) {
| |
| if (!a || !b || a.length < 2 || b.length < 2) return;
| |
| | |
| var lonA = toNumber(a[0], 0);
| |
| var latA = toNumber(a[1], 0);
| |
| var lonB = toNumber(b[0], 0);
| |
| var latB = toNumber(b[1], 0);
| |
| var dLon = normalizeLonDelta(lonA, lonB);
| |
| var dLat = latB - latA;
| |
| var steps = Math.max(1, Math.ceil(Math.max(Math.abs(dLon), Math.abs(dLat)) / 3));
| |
| var i;
| |
| | |
| for (i = 0; i < steps; i += 1) {
| |
| var t0 = i / steps;
| |
| var t1 = (i + 1) / steps;
| |
| var p0 = lonLatToVector3(THREE, lonA + dLon * t0, latA + dLat * t0, radius);
| |
| var p1 = lonLatToVector3(THREE, lonA + dLon * t1, latA + dLat * t1, radius);
| |
| | |
| positions.push(p0.x, p0.y, p0.z, p1.x, p1.y, p1.z);
| |
| }
| |
| }
| |
| | |
| function pushLineString(THREE, positions, coords, radius) {
| |
| var i;
| |
| if (!Array.isArray(coords) || coords.length < 2) return;
| |
| | |
| for (i = 0; i < coords.length - 1; i += 1) {
| |
| pushLineSegment(THREE, positions, coords[i], coords[i + 1], radius);
| |
| }
| |
| }
| |
| | |
| function pushPolygonRings(THREE, positions, rings, radius) {
| |
| if (!Array.isArray(rings)) return;
| |
| | |
| rings.forEach(function (ring) {
| |
| pushLineString(THREE, positions, ring, radius);
| |
| });
| |
| }
| |
| | |
| function pushGeometryLines(THREE, positions, geometry, radius) {
| |
| if (!geometry) return;
| |
| | |
| if (geometry.type === 'LineString') {
| |
| pushLineString(THREE, positions, geometry.coordinates, radius);
| |
| return;
| |
| }
| |
| | |
| if (geometry.type === 'MultiLineString') {
| |
| geometry.coordinates.forEach(function (line) {
| |
| pushLineString(THREE, positions, line, radius); | |
| });
| |
| return;
| |
| }
| |
| | |
| if (geometry.type === 'Polygon') {
| |
| pushPolygonRings(THREE, positions, geometry.coordinates, radius);
| |
| return;
| |
| }
| |
|
| |
|
| if (geometry.type === 'MultiPolygon') {
| | bodies.forEach(function (body) { |
| geometry.coordinates.forEach(function (polygon) {
| | var active = body.getAttribute('data-continent-panel') === continent; |
| pushPolygonRings(THREE, positions, polygon, radius);
| | body.classList.toggle('is-active', active); |
| });
| | if (active) { |
| return;
| | body.removeAttribute('hidden'); |
| }
| | body.setAttribute('aria-hidden', 'false'); |
| | | } else { |
| if (geometry.type === 'GeometryCollection') {
| | body.setAttribute('hidden', 'hidden'); |
| geometry.geometries.forEach(function (child) {
| | body.setAttribute('aria-hidden', 'true'); |
| pushGeometryLines(THREE, positions, child, radius);
| |
| });
| |
| }
| |
| }
| |
| | |
| function extractGeojsonFromPayload(payload, topojson, objectName) {
| |
| if (!payload) throw new Error('empty data');
| |
| | |
| if (payload.type === 'Topology') {
| |
| if (!topojson || typeof topojson.mesh !== 'function') {
| |
| throw new Error('topojson-client unavailable');
| |
| }
| |
| | |
| var objects = payload.objects || {};
| |
| var target = objects[objectName] || objects.countries || objects.land || objects[Object.keys(objects)[0]];
| |
| | |
| if (!target) throw new Error('topology object not found');
| |
| | |
| return topojson.mesh(payload, target, function (a, b) {
| |
| return a !== b;
| |
| });
| |
| }
| |
| | |
| if (payload.type === 'FeatureCollection') {
| |
| return payload;
| |
| }
| |
| | |
| if (payload.type === 'Feature') {
| |
| return {
| |
| type: 'FeatureCollection',
| |
| features: [payload]
| |
| };
| |
| }
| |
| | |
| if (payload.type && payload.coordinates) {
| |
| return payload;
| |
| }
| |
| | |
| throw new Error('unsupported map data');
| |
| }
| |
| | |
| function buildLineGeometryFromGeojson(THREE, geojson, radius) {
| |
| var positions = [];
| |
| | |
| if (geojson.type === 'FeatureCollection') {
| |
| geojson.features.forEach(function (feature) {
| |
| pushGeometryLines(THREE, positions, feature.geometry, radius);
| |
| });
| |
| } else if (geojson.type === 'Feature') {
| |
| pushGeometryLines(THREE, positions, geojson.geometry, radius);
| |
| } else {
| |
| pushGeometryLines(THREE, positions, geojson, radius);
| |
| }
| |
| | |
| var geometry = new THREE.BufferGeometry();
| |
| geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
| |
| return {
| |
| geometry: geometry,
| |
| segmentCount: positions.length / 6
| |
| };
| |
| }
| |
| | |
| function createEarthTexture(THREE) {
| |
| var canvas = document.createElement('canvas');
| |
| var size = 1024;
| |
| var ctx;
| |
| var gradient;
| |
| var i;
| |
| | |
| canvas.width = size;
| |
| canvas.height = size / 2;
| |
| ctx = canvas.getContext('2d');
| |
| | |
| ctx.fillStyle = '#08111a';
| |
| ctx.fillRect(0, 0, canvas.width, canvas.height);
| |
| | |
| gradient = ctx.createRadialGradient(size * 0.42, size * 0.20, 10, size * 0.45, size * 0.28, size * 0.50);
| |
| gradient.addColorStop(0, 'rgba(100,135,160,0.38)');
| |
| gradient.addColorStop(0.28, 'rgba(32,57,74,0.28)');
| |
| gradient.addColorStop(1, 'rgba(0,0,0,0)');
| |
| ctx.fillStyle = gradient;
| |
| ctx.fillRect(0, 0, canvas.width, canvas.height);
| |
| | |
| ctx.strokeStyle = 'rgba(255,255,255,0.030)';
| |
| ctx.lineWidth = 1;
| |
| | |
| for (i = 0; i <= 12; i += 1) {
| |
| var y = Math.round(i * canvas.height / 12) + 0.5;
| |
| ctx.beginPath();
| |
| ctx.moveTo(0, y);
| |
| ctx.lineTo(canvas.width, y);
| |
| ctx.stroke();
| |
| }
| |
| | |
| for (i = 0; i <= 24; i += 1) {
| |
| var x = Math.round(i * canvas.width / 24) + 0.5;
| |
| ctx.beginPath();
| |
| ctx.moveTo(x, 0);
| |
| ctx.lineTo(x, canvas.height);
| |
| ctx.stroke();
| |
| }
| |
| | |
| for (i = 0; i < 220; i += 1) {
| |
| var px = Math.random() * canvas.width;
| |
| var py = Math.random() * canvas.height;
| |
| var r = 1 + Math.random() * 2.8;
| |
| ctx.fillStyle = Math.random() > 0.5 ? 'rgba(80,105,93,0.18)' : 'rgba(95,83,70,0.16)';
| |
| ctx.beginPath();
| |
| ctx.ellipse(px, py, r * 6, r * 1.7, Math.random() * Math.PI, 0, Math.PI * 2);
| |
| ctx.fill();
| |
| }
| |
| | |
| var texture = new THREE.CanvasTexture(canvas);
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.needsUpdate = true;
| |
| return texture;
| |
| }
| |
| | |
| | |
| function loadImageTexture(THREE, url) {
| |
| return new Promise(function (resolve, reject) {
| |
| var loader = new THREE.TextureLoader();
| |
| | |
| loader.load(
| |
| url,
| |
| function (texture) {
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.wrapS = THREE.RepeatWrapping; | |
| texture.wrapT = THREE.ClampToEdgeWrapping;
| |
| texture.anisotropy = 4;
| |
| texture.needsUpdate = true;
| |
| resolve(texture);
| |
| },
| |
| undefined,
| |
| function (err) {
| |
| reject(err || new Error('texture load failed: ' + url));
| |
| }
| |
| );
| |
| });
| |
| }
| |
| | |
| function loadImageElement(url) {
| |
| return new Promise(function (resolve, reject) {
| |
| var image = new Image();
| |
| | |
| image.crossOrigin = 'anonymous';
| |
| image.decoding = 'async';
| |
| | |
| image.onload = function () {
| |
| resolve(image);
| |
| };
| |
| | |
| image.onerror = function () {
| |
| reject(new Error('image load failed: ' + url));
| |
| };
| |
| | |
| image.src = url;
| |
| });
| |
| }
| |
| | |
| function createCompositedEarthTextureLayers(THREE, baseUrl, layers, maxWidth) {
| |
| var effectiveLayers = Array.isArray(layers) ? layers.filter(function (layer) {
| |
| return !!(layer && layer.url);
| |
| }) : [];
| |
| | |
| return loadImageElement(baseUrl).then(function (base) {
| |
| return Promise.all(effectiveLayers.map(function (layer) {
| |
| return loadImageElement(layer.url)
| |
| .then(function (image) {
| |
| return { | |
| layer: layer,
| |
| image: image
| |
| };
| |
| })
| |
| .catch(function (err) {
| |
| if (window.console && console.warn) {
| |
| console.warn('CLBI earth composite layer skipped:', {
| |
| label: layer.label || 'overlay',
| |
| url: layer.url,
| |
| error: err
| |
| });
| |
| } | |
| return null;
| |
| });
| |
| })).then(function (loadedLayers) {
| |
| var width = Math.max(1024, Math.min(8192, Math.floor(maxWidth || 4096)));
| |
| var height = Math.max(512, Math.floor(width / 2));
| |
| var canvas = document.createElement('canvas');
| |
| var ctx;
| |
| var texture;
| |
| | |
| canvas.width = width;
| |
| canvas.height = height;
| |
| ctx = canvas.getContext('2d');
| |
| | |
| ctx.imageSmoothingEnabled = true;
| |
| ctx.imageSmoothingQuality = 'high';
| |
| | |
| ctx.clearRect(0, 0, width, height);
| |
| ctx.globalCompositeOperation = 'source-over';
| |
| ctx.globalAlpha = 1;
| |
| ctx.drawImage(base, 0, 0, width, height);
| |
| | |
| loadedLayers.forEach(function (entry) {
| |
| var layer;
| |
| var opacity;
| |
| var blend;
| |
| | |
| if (!entry || !entry.image) return;
| |
| | |
| layer = entry.layer;
| |
| opacity = Math.max(0, Math.min(1.4, layer.opacity == null ? 1 : layer.opacity));
| |
| blend = String(layer.blend || 'source-over').toLowerCase();
| |
| | |
| ctx.globalCompositeOperation = blend;
| |
| ctx.globalAlpha = opacity;
| |
| if (layer.filter && typeof ctx.filter !== 'undefined') {
| |
| ctx.filter = String(layer.filter);
| |
| } else if (typeof ctx.filter !== 'undefined') {
| |
| ctx.filter = 'none';
| |
| } | | } |
| ctx.drawImage(entry.image, 0, 0, width, height);
| |
| }); | | }); |
|
| |
| ctx.globalCompositeOperation = 'source-over';
| |
| ctx.globalAlpha = 1;
| |
| if (typeof ctx.filter !== 'undefined') {
| |
| ctx.filter = 'none';
| |
| }
| |
|
| |
| texture = new THREE.CanvasTexture(canvas);
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.wrapS = THREE.RepeatWrapping;
| |
| texture.wrapT = THREE.ClampToEdgeWrapping;
| |
| texture.anisotropy = 6;
| |
| texture.needsUpdate = true;
| |
|
| |
| return texture;
| |
| });
| |
| });
| |
| }
| |
|
| |
| function createFallbackPlanetTexture(THREE, colorA, colorB) {
| |
| var canvas = document.createElement('canvas');
| |
| var size = 512;
| |
| var ctx;
| |
| var gradient;
| |
| var i;
| |
|
| |
| canvas.width = size;
| |
| canvas.height = size / 2;
| |
| ctx = canvas.getContext('2d');
| |
|
| |
| gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
| |
| gradient.addColorStop(0, colorA || '#777777');
| |
| gradient.addColorStop(1, colorB || '#151515');
| |
|
| |
| ctx.fillStyle = gradient;
| |
| ctx.fillRect(0, 0, canvas.width, canvas.height);
| |
|
| |
| for (i = 0; i < 18; i += 1) {
| |
| ctx.fillStyle = i % 2 ? 'rgba(255,255,255,0.055)' : 'rgba(0,0,0,0.095)';
| |
| ctx.fillRect(0, Math.round(i * canvas.height / 18), canvas.width, Math.max(1, Math.round(canvas.height / 46)));
| |
| }
| |
|
| |
| var texture = new THREE.CanvasTexture(canvas);
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.wrapS = THREE.RepeatWrapping;
| |
| texture.wrapT = THREE.ClampToEdgeWrapping;
| |
| texture.needsUpdate = true;
| |
| return texture;
| |
| }
| |
|
| |
| function createSaturnRingTexture(THREE) {
| |
| var canvas = document.createElement('canvas');
| |
| var size = 512;
| |
| var ctx;
| |
| var image;
| |
| var data;
| |
| var cx = size * 0.5;
| |
| var cy = size * 0.5;
| |
| var maxR = size * 0.5;
| |
| var texture;
| |
| var x;
| |
| var y;
| |
|
| |
| canvas.width = size;
| |
| canvas.height = size;
| |
| ctx = canvas.getContext('2d');
| |
| image = ctx.createImageData(size, size);
| |
| data = image.data;
| |
|
| |
| for (y = 0; y < size; y += 1) {
| |
| for (x = 0; x < size; x += 1) {
| |
| var dx = (x - cx) / maxR;
| |
| var dy = (y - cy) / maxR;
| |
| var r = Math.sqrt(dx * dx + dy * dy);
| |
| var idx = (y * size + x) * 4;
| |
| var band;
| |
| var gap;
| |
| var alpha;
| |
| var warm;
| |
|
| |
| if (r < 0.200 || r > 0.500) {
| |
| data[idx + 3] = 0;
| |
| continue;
| |
| }
| |
|
| |
| band = 0.58
| |
| + 0.22 * Math.sin(r * 92.0)
| |
| + 0.13 * Math.sin(r * 177.0 + 0.6)
| |
| + 0.07 * Math.sin(r * 317.0 - 1.1);
| |
| gap = (r > 0.365 && r < 0.386) ? 0.22 : 1.0;
| |
| alpha = Math.max(0, Math.min(1, (band * 1.28 + 0.08) * gap));
| |
| warm = Math.max(0, Math.min(1, (0.50 - r) / 0.300));
| |
|
| |
| data[idx + 0] = Math.round(216 + warm * 38);
| |
| data[idx + 1] = Math.round(186 + warm * 44);
| |
| data[idx + 2] = Math.round(126 + warm * 48);
| |
| data[idx + 3] = Math.round(alpha * 248);
| |
| } | | } |
| }
| |
|
| |
| ctx.putImageData(image, 0, 0);
| |
| texture = new THREE.CanvasTexture(canvas);
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.needsUpdate = true;
| |
| return texture;
| |
| }
| |
|
| |
|
| |
| function configureSaturnRingMap(THREE, texture) {
| |
| if (!texture) return texture;
| |
|
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.wrapS = THREE.ClampToEdgeWrapping;
| |
| texture.wrapT = THREE.ClampToEdgeWrapping;
| |
| texture.anisotropy = 6;
| |
| texture.needsUpdate = true;
| |
| return texture;
| |
| }
| |
|
| |
| function loadSaturnRingTexture(THREE, url) {
| |
| return loadLimitedImageTexture(THREE, url, 2048).then(function (texture) {
| |
| return configureSaturnRingMap(THREE, texture);
| |
| });
| |
| }
| |
|
| |
| function createSaturnRingGeometry(THREE, innerRadius, outerRadius) {
| |
| var geometry = new THREE.RingGeometry(innerRadius, outerRadius, 256, 32);
| |
| var position = geometry.attributes.position;
| |
| var uv = geometry.attributes.uv;
| |
| var range = Math.max(0.0001, outerRadius - innerRadius);
| |
| var i;
| |
|
| |
| /*
| |
| * The Solar System Scope / Commons file is a radial strip: left-to-right is
| |
| * inner-to-outer ring structure. Three.js RingGeometry's default UVs map
| |
| * the whole square, which turns the strip into a flat painted oval. Rewrite
| |
| * the U coordinate from each vertex's local radius so the real ring bands
| |
| * wrap around Saturn concentrically.
| |
| */
| |
| for (i = 0; i < position.count; i += 1) {
| |
| var x = position.getX(i);
| |
| var y = position.getY(i);
| |
| var r = Math.sqrt(x * x + y * y);
| |
| var u = Math.max(0, Math.min(1, (r - innerRadius) / range));
| |
| uv.setXY(i, u, 0.5);
| |
| }
| |
|
| |
| uv.needsUpdate = true;
| |
| return geometry;
| |
| }
| |
|
| |
| function createSolarInnerGlowTexture(THREE) {
| |
| var canvas = document.createElement('canvas');
| |
| var size = 384;
| |
| var ctx = canvas.getContext('2d');
| |
| var gradient;
| |
| var texture;
| |
|
| |
| canvas.width = size;
| |
| canvas.height = size;
| |
|
| |
| gradient = ctx.createRadialGradient(
| |
| size * 0.5, size * 0.5, 0,
| |
| size * 0.5, size * 0.5, size * 0.5
| |
| );
| |
|
| |
| gradient.addColorStop(0.000, 'rgba(255,252,232,1.00)');
| |
| gradient.addColorStop(0.090, 'rgba(255,232,168,0.96)');
| |
| gradient.addColorStop(0.240, 'rgba(255,182,82,0.70)');
| |
| gradient.addColorStop(0.430, 'rgba(255,126,28,0.36)');
| |
| gradient.addColorStop(0.660, 'rgba(232,96,16,0.12)');
| |
| gradient.addColorStop(1.000, 'rgba(0,0,0,0.00)');
| |
|
| |
| ctx.fillStyle = gradient;
| |
| ctx.fillRect(0, 0, size, size);
| |
|
| |
| texture = new THREE.CanvasTexture(canvas);
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.needsUpdate = true;
| |
| return texture;
| |
| }
| |
|
| |
| function createSolarCoronaTexture(THREE) {
| |
| var canvas = document.createElement('canvas');
| |
| var size = 512;
| |
| var ctx;
| |
| var image;
| |
| var data;
| |
| var cx = size * 0.5;
| |
| var cy = size * 0.5;
| |
| var maxR = size * 0.5;
| |
| var texture;
| |
| var x;
| |
| var y;
| |
|
| |
| canvas.width = size;
| |
| canvas.height = size;
| |
| ctx = canvas.getContext('2d');
| |
| image = ctx.createImageData(size, size);
| |
| data = image.data;
| |
|
| |
| for (y = 0; y < size; y += 1) {
| |
| for (x = 0; x < size; x += 1) {
| |
| var dx = (x - cx) / maxR;
| |
| var dy = (y - cy) / maxR;
| |
| var r = Math.sqrt(dx * dx + dy * dy);
| |
| var idx = (y * size + x) * 4;
| |
| var angle;
| |
| var noise;
| |
| var base;
| |
| var alpha;
| |
| var warm;
| |
|
| |
| if (r >= 1) {
| |
| data[idx + 3] = 0;
| |
| continue;
| |
| }
| |
|
| |
| angle = Math.atan2(dy, dx);
| |
| noise = 0.90
| |
| + 0.10 * Math.sin(angle * 7.0 + r * 9.0)
| |
| + 0.06 * Math.sin(angle * 13.0 - r * 16.0)
| |
| + 0.04 * Math.sin(angle * 29.0 + r * 25.0);
| |
| base = Math.pow(Math.max(0, 1 - r), 2.65);
| |
| alpha = Math.max(0, Math.min(1, base * noise));
| |
| warm = Math.max(0, Math.min(1, 1 - r * 0.9));
| |
|
| |
| data[idx + 0] = Math.round(255);
| |
| data[idx + 1] = Math.round(170 + 45 * warm);
| |
| data[idx + 2] = Math.round(72 + 26 * warm);
| |
| data[idx + 3] = Math.round(alpha * 255);
| |
| }
| |
| }
| |
|
| |
| ctx.putImageData(image, 0, 0);
| |
| texture = new THREE.CanvasTexture(canvas);
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.needsUpdate = true;
| |
| return texture;
| |
| }
| |
|
| |
| function loadLimitedImageTexture(THREE, url, maxWidth) {
| |
| return new Promise(function (resolve, reject) {
| |
| var image = new Image();
| |
| var limit = Math.max(64, Math.min(1024, Math.floor(maxWidth || 512)));
| |
|
| |
| image.crossOrigin = 'anonymous';
| |
| image.decoding = 'async';
| |
|
| |
| image.onload = function () {
| |
| var naturalW = image.naturalWidth || image.width || limit;
| |
| var naturalH = image.naturalHeight || image.height || Math.max(1, Math.floor(limit / 2));
| |
| var scale = Math.min(1, limit / naturalW);
| |
| var width = Math.max(1, Math.round(naturalW * scale));
| |
| var height = Math.max(1, Math.round(naturalH * scale));
| |
| var canvas = document.createElement('canvas');
| |
| var ctx;
| |
| var texture;
| |
|
| |
| canvas.width = width;
| |
| canvas.height = height;
| |
| ctx = canvas.getContext('2d');
| |
| ctx.drawImage(image, 0, 0, width, height);
| |
|
| |
| texture = new THREE.CanvasTexture(canvas);
| |
| texture.colorSpace = THREE.SRGBColorSpace;
| |
| texture.wrapS = THREE.RepeatWrapping;
| |
| texture.wrapT = THREE.ClampToEdgeWrapping;
| |
| texture.anisotropy = 2;
| |
| texture.needsUpdate = true;
| |
| resolve(texture);
| |
| };
| |
|
| |
| image.onerror = function () {
| |
| reject(new Error('limited texture load failed: ' + url));
| |
| };
| |
|
| |
| image.src = url;
| |
| });
| |
| }
| |
|
| |
| function clamp01(value) {
| |
| return Math.max(0, Math.min(1, value));
| |
| }
| |
|
| |
| function easeOutCubic(t) {
| |
| t = clamp01(t);
| |
| return 1 - Math.pow(1 - t, 3);
| |
| }
| |
|
| |
| function nearestEquivalentAngle(current, target) {
| |
| var full = Math.PI * 2;
| |
| return target + Math.round((current - target) / full) * full;
| |
| }
| |
|
| |
| function canonicalAngle(value) {
| |
| var full = Math.PI * 2;
| |
| var angle = value % full;
| |
|
| |
| if (angle > Math.PI) angle -= full;
| |
| if (angle < -Math.PI) angle += full;
| |
|
| |
| return angle;
| |
| }
| |
|
| |
| function inverseLerp(a, b, value) {
| |
| if (a === b) return 0;
| |
| return (value - a) / (b - a);
| |
| }
| |
|
| |
| function createStars(THREE, count) {
| |
| var geometry = new THREE.BufferGeometry();
| |
| var positions = [];
| |
| var i;
| |
|
| |
| for (i = 0; i < count; i += 1) {
| |
| var r = 8 + Math.random() * 16;
| |
| var theta = Math.random() * Math.PI * 2;
| |
| var phi = Math.acos((Math.random() * 2) - 1);
| |
|
| |
| positions.push(
| |
| r * Math.sin(phi) * Math.cos(theta),
| |
| r * Math.cos(phi),
| |
| r * Math.sin(phi) * Math.sin(theta)
| |
| );
| |
| }
| |
|
| |
| geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
| |
|
| |
| return new THREE.Points(
| |
| geometry,
| |
| new THREE.PointsMaterial({
| |
| color: 0xd8d8d8,
| |
| size: 0.018,
| |
| transparent: true,
| |
| opacity: 0.62,
| |
| depthWrite: false
| |
| })
| |
| );
| |
| }
| |
|
| |
| function NationsGlobe(root) {
| |
| this.root = root;
| |
| this.stage = null;
| |
| this.loading = null;
| |
| this.error = null;
| |
| this.status = null;
| |
| this.readout = null;
| |
| this.autoButton = null;
| |
| this.resetButton = null;
| |
| this.THREE = null;
| |
| this.scene = null;
| |
| this.camera = null;
| |
| this.renderer = null;
| |
| this.group = null;
| |
| this.starGroup = null;
| |
| this.celestialGroup = null;
| |
| this.celestialPlanets = [];
| |
| this.starDriftY = 0;
| |
| this.celestialDriftY = 0;
| |
| this.celestialSun = null;
| |
| this.celestialSunGlowA = null;
| |
| this.celestialSunGlowB = null;
| |
| this.celestialSunCorona = null;
| |
| this.sunOccultRendered = 0;
| |
| this.sunOccultMotion = null;
| |
| this.moon = null;
| |
| this.moonShell = null;
| |
| this.earth = null;
| |
| this.earthMaterial = null;
| |
| this.clouds = null;
| |
| this.cloudMaterial = null;
| |
| this.lakesOverlay = null;
| |
| this.raycaster = null;
| |
| this.pointer = null;
| |
| this.pointerInside = false;
| |
| this.pointerClientX = null;
| |
| this.pointerClientY = null;
| |
| this.pointerDownX = null;
| |
| this.pointerDownY = null;
| |
| this.pointerDownNationPage = null;
| |
| this.clickMoveTolerance = toNumber(root.getAttribute('data-nation-click-tolerance'), 5);
| |
| this.nationHitUrl = root.getAttribute('data-polygon-hit-url') || '';
| |
| this.nationLinkMapUrl = root.getAttribute('data-nation-link-map-url') || '';
| |
| this.nationLinkBase = root.getAttribute('data-nation-link-base') || '/index.php/';
| |
| this.nationHitFeatures = [];
| |
| this.nationLinkMap = {};
| |
| this.currentNation = null;
| |
| this.nationTooltip = null;
| |
| this.radius = 1;
| |
| this.rotationX = toNumber(root.getAttribute('data-rotation-x'), -0.18);
| |
| this.rotationY = toNumber(root.getAttribute('data-rotation-y'), -0.48);
| |
| this.cameraZ = toNumber(root.getAttribute('data-camera-z'), 3.75);
| |
| this.cameraZMin = toNumber(root.getAttribute('data-camera-z-min'), 1.23);
| |
| this.cameraZMax = toNumber(root.getAttribute('data-camera-z-max'), 5.2);
| |
| this.resetRotationX = toNumber(root.getAttribute('data-reset-rotation-x'), this.rotationX);
| |
| this.resetRotationY = toNumber(root.getAttribute('data-reset-rotation-y'), this.rotationY);
| |
| this.resetCameraZ = toNumber(root.getAttribute('data-reset-camera-z'), this.cameraZ);
| |
| if (!root.getAttribute('data-camera-z') && root.getAttribute('data-reset-camera-z') === '3.05') {
| |
| this.resetCameraZ = this.cameraZ;
| |
| }
| |
| this.resetDuration = toNumber(root.getAttribute('data-reset-duration'), 520);
| |
| this.resetMotion = null;
| |
| this.terrainUrl = root.getAttribute('data-terrain-url') || '';
| |
| this.cloudsUrl = root.getAttribute('data-clouds-url') || '';
| |
| this.boundarySurfaceOffset = toNumber(root.getAttribute('data-boundary-surface-offset'), 0.0022);
| |
| this.polygonOverlayEnabled = root.getAttribute('data-polygon-overlay') !== 'false';
| |
| this.polygonOverlayUrl = root.getAttribute('data-polygon-overlay-url') || '';
| |
| this.polygonOverlaySize = toNumber(root.getAttribute('data-polygon-overlay-size'), 4096);
| |
| this.earthCompositeSize = toNumber(root.getAttribute('data-earth-composite-size'), this.polygonOverlaySize || 4096);
| |
| this.polygonOverlayOpacity = toNumber(root.getAttribute('data-polygon-overlay-opacity'), 1);
| |
| this.polygonOverlayBlend = root.getAttribute('data-polygon-overlay-blend') || 'source-over';
| |
| this.polygonOverlayFilter = root.getAttribute('data-polygon-overlay-filter') || 'none';
| |
| this.reliefOverlayUrl = root.getAttribute('data-relief-overlay-url') || '';
| |
| this.reliefOverlayOpacity = toNumber(root.getAttribute('data-relief-overlay-opacity'), 1);
| |
| this.reliefOverlayBlend = root.getAttribute('data-relief-overlay-blend') || 'source-over';
| |
| this.reliefOverlayFilter = root.getAttribute('data-relief-overlay-filter') || 'none';
| |
| this.lakesOverlayUrl = root.getAttribute('data-lakes-overlay-url') || '';
| |
| this.lakesOverlayOpacity = toNumber(root.getAttribute('data-lakes-overlay-opacity'), 1);
| |
| this.lakesSurfaceOffset = toNumber(root.getAttribute('data-lakes-surface-offset'), 0.0029);
| |
| this.cloudOpacityNear = toNumber(root.getAttribute('data-cloud-opacity-near'), 0.10);
| |
| this.cloudOpacityFar = toNumber(root.getAttribute('data-cloud-opacity-far'), 0.90);
| |
| this.cloudDriftSpeed = toNumber(root.getAttribute('data-cloud-drift-speed'), 0.000010);
| |
| this.cloudTiltDrift = toNumber(root.getAttribute('data-cloud-tilt-drift'), 0.006);
| |
| this.celestialEnabled = root.getAttribute('data-celestial-enabled') !== 'false';
| |
| this.celestialTextureSize = toNumber(root.getAttribute('data-celestial-texture-size'), 512);
| |
| this.sunTextureSize = toNumber(root.getAttribute('data-sun-texture-size'), 512);
| |
| this.moonTextureSize = toNumber(root.getAttribute('data-moon-texture-size'), 512);
| |
| this.moonOpacityFar = toNumber(root.getAttribute('data-moon-opacity-far'), 1.00);
| |
| this.moonOpacityNear = toNumber(root.getAttribute('data-moon-opacity-near'), 0.00);
| |
| this.moonShellOpacityFar = toNumber(root.getAttribute('data-moon-shell-opacity-far'), 0.040);
| |
| this.moonShellOpacityNear = toNumber(root.getAttribute('data-moon-shell-opacity-near'), 0.00);
| |
| this.moonVanishZ = toNumber(root.getAttribute('data-moon-vanish-z'), 3.15);
| |
| this.moonFullVisibleZ = toNumber(root.getAttribute('data-moon-full-visible-z'), 4.20);
| |
| this.moonEarthFadeOpacity = toNumber(root.getAttribute('data-moon-earth-fade-opacity'), 0.08);
| |
| this.moonEarthFadeMargin = toNumber(root.getAttribute('data-moon-earth-fade-margin'), 28);
| |
| this.moonFadeDuration = toNumber(root.getAttribute('data-moon-fade-duration'), this.resetDuration);
| |
| this.sunOccultFadeDuration = toNumber(root.getAttribute('data-sun-occult-fade-duration'), this.moonFadeDuration);
| |
| this.moonRenderedOpacity = null;
| |
| this.moonRenderedShellOpacity = null;
| |
| this.moonFadeMotion = null;
| |
| this.celestialParallax = toNumber(root.getAttribute('data-celestial-parallax'), 0.070);
| |
| this.starParallax = toNumber(root.getAttribute('data-star-parallax'), 0.012);
| |
| this.starDriftSpeed = toNumber(root.getAttribute('data-star-drift-speed'), 0);
| |
| this.celestialDriftSpeed = toNumber(root.getAttribute('data-celestial-drift-speed'), 0);
| |
| this.autoRotate = root.getAttribute('data-auto-rotate') !== 'false';
| |
| this.resetAutoRotate = this.autoRotate;
| |
| this.autoRotateSpeed = toNumber(root.getAttribute('data-auto-rotate-speed'), 0.000085);
| |
| this.autoResumeDuration = toNumber(root.getAttribute('data-auto-resume-duration'), Math.max(1600, this.resetDuration * 3.5));
| |
| this.autoResumeMinFactor = toNumber(root.getAttribute('data-auto-resume-min-factor'), 0.06);
| |
| this.autoResumeMotion = null;
| |
| this.dragging = false;
| |
| this.lastX = 0;
| |
| this.lastY = 0;
| |
| this.lastMoveTime = 0;
| |
| this.velocityX = 0;
| |
| this.velocityY = 0;
| |
| this.dragSensitivityY = toNumber(root.getAttribute('data-drag-sensitivity-y'), 0.0030);
| |
| this.dragSensitivityX = toNumber(root.getAttribute('data-drag-sensitivity-x'), 0.0020);
| |
| this.inertiaStrength = toNumber(root.getAttribute('data-inertia-strength'), 0.58);
| |
| this.inertiaDamping = toNumber(root.getAttribute('data-inertia-damping'), 0.993);
| |
| this.inertiaMinVelocity = toNumber(root.getAttribute('data-inertia-min-velocity'), 0.000085);
| |
| this.inertiaReleaseWindow = toNumber(root.getAttribute('data-inertia-release-window'), 150);
| |
| this.stageEventsBound = false;
| |
| this.frame = null;
| |
| this.disposed = false;
| |
| this.segmentCount = 0;
| |
| this.lastTime = performance.now();
| |
| }
| |
|
| |
| NationsGlobe.prototype.ensureDom = function () {
| |
| var root = this.root;
| |
| var title = root.getAttribute('data-title') || 'TERRITORY GLOBE';
| |
| var era = root.getAttribute('data-era') || 'REAL WORLD / 110M';
| |
| var topbarHtml;
| |
| var bottombarHtml;
| |
|
| |
| root.classList.add('clbi-nations-globe-window');
| |
|
| |
| topbarHtml =
| |
| '<div class="clbi-nations-globe-topbar">' +
| |
| '<span class="clbi-nations-globe-title">' + escapeHtml(title) + '</span>' +
| |
| '<span class="clbi-nations-globe-status">SYNC</span>' +
| |
| '</div>';
| |
|
| |
| bottombarHtml =
| |
| '<div class="clbi-nations-globe-bottombar">' +
| |
| '<span class="clbi-nations-globe-readout">' + escapeHtml(era) + '</span>' +
| |
| '<span class="clbi-nations-globe-controls">' +
| |
| '<button type="button" class="clbi-nations-globe-btn clbi-nations-globe-auto">AUTO</button>' +
| |
| '<button type="button" class="clbi-nations-globe-btn clbi-nations-globe-reset">RESET</button>' +
| |
| '</span>' +
| |
| '</div>';
| |
|
| |
| if (!root.querySelector('.clbi-nations-globe-stage')) {
| |
| root.innerHTML =
| |
| '<div class="clbi-nations-globe-stage" aria-label="3D globe viewport"></div>' +
| |
| '<div class="clbi-nations-globe-vignette" aria-hidden="true"></div>' +
| |
| topbarHtml +
| |
| bottombarHtml +
| |
| '<div class="clbi-nations-globe-loading">LOADING REAL-WORLD TERRITORY DATA</div>' +
| |
| '<div class="clbi-nations-globe-error"></div>';
| |
| } else {
| |
| if (!root.querySelector('.clbi-nations-globe-vignette')) {
| |
| root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-vignette" aria-hidden="true"></div>');
| |
| }
| |
|
| |
| if (!root.querySelector('.clbi-nations-globe-topbar')) {
| |
| root.insertAdjacentHTML('beforeend', topbarHtml);
| |
| }
| |
|
| |
| if (!root.querySelector('.clbi-nations-globe-bottombar')) {
| |
| root.insertAdjacentHTML('beforeend', bottombarHtml);
| |
| }
| |
|
| |
| if (!root.querySelector('.clbi-nations-globe-loading')) {
| |
| root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-loading">LOADING REAL-WORLD TERRITORY DATA</div>');
| |
| }
| |
|
| |
| if (!root.querySelector('.clbi-nations-globe-error')) {
| |
| root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-error"></div>');
| |
| }
| |
| }
| |
|
| |
| this.stage = root.querySelector('.clbi-nations-globe-stage');
| |
| this.loading = root.querySelector('.clbi-nations-globe-loading');
| |
| this.error = root.querySelector('.clbi-nations-globe-error');
| |
| this.status = root.querySelector('.clbi-nations-globe-status');
| |
| this.readout = root.querySelector('.clbi-nations-globe-readout');
| |
| if (!root.querySelector('.clbi-nations-globe-nation-tooltip')) {
| |
| root.insertAdjacentHTML('beforeend', '<div class="clbi-nations-globe-nation-tooltip" aria-hidden="true"></div>');
| |
| }
| |
|
| |
| this.autoButton = root.querySelector('.clbi-nations-globe-auto');
| |
| this.resetButton = root.querySelector('.clbi-nations-globe-reset');
| |
| this.nationTooltip = root.querySelector('.clbi-nations-globe-nation-tooltip');
| |
| ensureNationsGlobeInteractionStyle();
| |
|
| |
| this.syncAutoButton();
| |
|
| |
| if (this.readout && !this.readout.textContent) {
| |
| this.readout.textContent = era;
| |
| }
| |
| };
| |
|
| |
| NationsGlobe.prototype.rehydrateDom = function () {
| |
| if (!this.root || !document.body.contains(this.root)) return false;
| |
|
| |
| this.ensureDom();
| |
| this.bindControlEvents();
| |
| this.resize();
| |
|
| |
| return true;
| |
| };
| |
|
| |
|
| |
|
| NationsGlobe.prototype.setStatus = function (text) {
| | tabs.forEach(function (tab) { |
| if (this.status) this.status.textContent = text || '';
| | tab.setAttribute('role', 'tab'); |
| };
| | tab.setAttribute('tabindex', '0'); |
| | | tab.addEventListener('click', function () { |
| NationsGlobe.prototype.setReadout = function (text) {
| | activate(tab.getAttribute('data-continent')); |
| if (this.readout) this.readout.textContent = text || '';
| |
| };
| |
| | |
| NationsGlobe.prototype.setError = function (err) {
| |
| var message = err && err.message ? err.message : String(err || 'unknown error');
| |
| this.root.classList.add('has-error');
| |
| this.root.classList.remove('is-ready');
| |
| if (this.error) this.error.textContent = 'GLOBE ERROR: ' + message;
| |
| this.setStatus('ERROR');
| |
| };
| |
| | |
| NationsGlobe.prototype.init = function () {
| |
| var self = this;
| |
| | |
| this.ensureDom();
| |
| this.setStatus('LIBRARY');
| |
| | |
| return Promise.all([loadThree(), loadTopojson()])
| |
| .then(function (libs) {
| |
| self.THREE = libs[0];
| |
| self.topojson = libs[1];
| |
| self.createScene();
| |
| self.bindEvents();
| |
| return self.loadOptionalTextures().then(function () {
| |
| return self.loadBoundaryData(); | |
| }).then(function () {
| |
| return self.loadNationInteractionData();
| |
| }); | | }); |
| })
| | tab.addEventListener('keydown', function (event) { |
| .then(function () {
| | if (event.key === 'Enter' || event.key === ' ') { |
| self.root.classList.add('is-ready'); | | event.preventDefault(); |
| self.setStatus('READY');
| | activate(tab.getAttribute('data-continent')); |
| self.animate(performance.now());
| | } |
| })
| |
| .catch(function (err) {
| |
| self.setError(err);
| |
| });
| |
| };
| |
| | |
| | |
| NationsGlobe.prototype.addMoon = function (texture) {
| |
| var THREE = this.THREE;
| |
| var def = CELESTIAL_MOON_DEF;
| |
| var material;
| |
| var mesh;
| |
| var shell;
| |
| | |
| if (!this.group || !texture) return;
| |
| | |
| material = new THREE.MeshPhongMaterial({
| |
| map: texture,
| |
| color: 0xffffff,
| |
| transparent: true,
| |
| opacity: this.moonOpacityFar,
| |
| depthWrite: false,
| |
| depthTest: true,
| |
| shininess: 2,
| |
| specular: new THREE.Color(0x050505),
| |
| emissive: new THREE.Color(0x010101)
| |
| });
| |
| | |
| mesh = new THREE.Mesh(
| |
| new THREE.SphereGeometry(def.radius, 42, 28),
| |
| material
| |
| );
| |
| | |
| mesh.position.set(def.position[0], def.position[1], def.position[2]);
| |
| mesh.renderOrder = 20;
| |
| | |
| shell = new THREE.Mesh(
| |
| new THREE.SphereGeometry(def.radius * 1.022, 36, 24),
| |
| new THREE.MeshBasicMaterial({
| |
| color: 0xd8d8d8,
| |
| transparent: true,
| |
| opacity: this.moonShellOpacityFar,
| |
| side: THREE.BackSide,
| |
| depthWrite: false
| |
| })
| |
| );
| |
| shell.position.copy(mesh.position);
| |
| shell.renderOrder = 19;
| |
| | |
| this.group.add(shell);
| |
| this.group.add(mesh);
| |
| | |
| this.moon = mesh;
| |
| this.moonShell = shell;
| |
| this.updateMoonOpacity();
| |
| };
| |
| | |
| NationsGlobe.prototype.loadMoon = function () {
| |
| var self = this;
| |
| var THREE = this.THREE;
| |
| var textureSize = Math.max(64, Math.min(1024, Math.floor(this.moonTextureSize || 512)));
| |
| | |
| loadLimitedImageTexture(THREE, CELESTIAL_MOON_TEXTURE_URL, textureSize)
| |
| .then(function (texture) {
| |
| self.addMoon(texture);
| |
| })
| |
| .catch(function (err) {
| |
| console.warn('CLBI moon texture failed:', err);
| |
| self.addMoon(createFallbackPlanetTexture(THREE, CELESTIAL_MOON_DEF.fallbackA, CELESTIAL_MOON_DEF.fallbackB));
| |
| });
| |
| };
| |
| | |
| NationsGlobe.prototype.resolveCelestialEclipticPosition = function (def) {
| |
| var plane = CELESTIAL_ECLIPTIC;
| |
| var orbitRadius = def.orbitRadius || 0;
| |
| var angle = def.orbitAngle || 0;
| |
| var x = Math.cos(angle) * orbitRadius;
| |
| var z = plane.depth + Math.sin(angle) * orbitRadius * plane.depthScale;
| |
| var y = x * plane.tiltYPerX + Math.sin(angle) * orbitRadius * (def.inclination || 0);
| |
| | |
| return [x, y, z];
| |
| };
| |
| | |
| NationsGlobe.prototype.addCelestialPlanet = function (def, texture) {
| |
| var THREE = this.THREE;
| |
| var self = this;
| |
| var mesh;
| |
| var material;
| |
| var shell;
| |
| var shellColor;
| |
| var ring;
| |
| var ringGlow;
| |
| var ringMaterial;
| |
| var position;
| |
| | |
| if (!this.celestialGroup || !def || !texture) return;
| |
| | |
| material = new THREE.MeshPhongMaterial({
| |
| map: texture,
| |
| color: 0xffffff,
| |
| transparent: true,
| |
| opacity: def.opacity,
| |
| depthWrite: false,
| |
| depthTest: true,
| |
| shininess: 3,
| |
| specular: new THREE.Color(0x050505),
| |
| emissive: new THREE.Color(0x020202)
| |
| });
| |
| | |
| mesh = new THREE.Mesh(
| |
| new THREE.SphereGeometry(def.radius, 42, 28),
| |
| material
| |
| );
| |
| | |
| position = this.resolveCelestialEclipticPosition(def);
| |
| mesh.position.set(position[0], position[1], position[2]);
| |
| mesh.renderOrder = 1;
| |
| this.celestialGroup.add(mesh);
| |
| | |
| if (def.ring) {
| |
| var ringInnerRadius = def.radius * (def.ringInnerScale || 1.34);
| |
| var ringOuterRadius = def.radius * (def.ringOuterScale || 2.58);
| |
| | |
| ringMaterial = new THREE.MeshBasicMaterial({
| |
| map: configureSaturnRingMap(THREE, createSaturnRingTexture(THREE)),
| |
| color: 0xffffff,
| |
| transparent: true,
| |
| opacity: def.ringOpacity || 0.86,
| |
| side: THREE.DoubleSide,
| |
| depthWrite: false,
| |
| depthTest: true,
| |
| blending: THREE.NormalBlending
| |
| });
| |
| | |
| ring = new THREE.Mesh(
| |
| createSaturnRingGeometry(THREE, ringInnerRadius, ringOuterRadius),
| |
| ringMaterial
| |
| );
| |
| ring.position.copy(mesh.position);
| |
| ring.rotation.x = def.ringTiltX || 1.08;
| |
| ring.rotation.y = def.ringTiltY || 0.18;
| |
| ring.rotation.z = def.ringTiltZ || -0.34;
| |
| ring.renderOrder = 1.8;
| |
| this.celestialGroup.add(ring);
| |
| | |
| if (def.ringTextureUrl) {
| |
| loadSaturnRingTexture(THREE, def.ringTextureUrl).then(function (ringTexture) {
| |
| if (!ringMaterial) return;
| |
| ringMaterial.map = ringTexture;
| |
| ringMaterial.needsUpdate = true; | |
| }).catch(function (err) {
| |
| console.warn('CLBI Saturn ring texture failed:', err);
| |
| }); | | }); |
| }
| |
| }
| |
|
| |
| shellColor = new THREE.Color(def.fallbackA || '#707070');
| |
|
| |
| shell = new THREE.Mesh(
| |
| new THREE.SphereGeometry(def.radius * 1.028, 36, 24),
| |
| new THREE.MeshBasicMaterial({
| |
| color: shellColor,
| |
| transparent: true,
| |
| opacity: Math.max(0.05, Math.min(0.12, (def.opacity || 0.4) * 0.20)),
| |
| side: THREE.BackSide,
| |
| depthWrite: false
| |
| })
| |
| );
| |
| shell.position.copy(mesh.position);
| |
| shell.renderOrder = 0;
| |
| this.celestialGroup.add(shell);
| |
|
| |
| this.celestialPlanets.push({
| |
| mesh: mesh,
| |
| shell: shell,
| |
| ring: ring,
| |
| ringGlow: ringGlow,
| |
| speed: def.rotationSpeed || 0
| |
| });
| |
| };
| |
|
| |
| NationsGlobe.prototype.addCelestialSun = function (texture) {
| |
| var THREE = this.THREE;
| |
| var def = CELESTIAL_SUN_DEF;
| |
| var position = this.resolveCelestialEclipticPosition(def);
| |
| var core;
| |
| var innerGlow;
| |
| var outerGlow;
| |
| var coreMaterial;
| |
| var innerGlowMaterial;
| |
| var outerGlowMaterial;
| |
|
| |
| if (!this.celestialGroup) return;
| |
|
| |
| coreMaterial = new THREE.MeshBasicMaterial({
| |
| map: texture || null,
| |
| color: def.color,
| |
| transparent: true,
| |
| opacity: def.coreOpacity,
| |
| depthWrite: false
| |
| });
| |
|
| |
| core = new THREE.Mesh(
| |
| new THREE.SphereGeometry(def.radius, 48, 32),
| |
| coreMaterial
| |
| );
| |
| core.position.set(position[0], position[1], position[2]);
| |
| core.renderOrder = 3;
| |
|
| |
| innerGlowMaterial = new THREE.SpriteMaterial({
| |
| map: createSolarInnerGlowTexture(THREE),
| |
| color: 0xffffff,
| |
| transparent: true,
| |
| opacity: def.innerGlowOpacity,
| |
| depthWrite: false,
| |
| depthTest: true,
| |
| blending: THREE.AdditiveBlending
| |
| });
| |
|
| |
| innerGlow = new THREE.Sprite(innerGlowMaterial);
| |
| innerGlow.position.copy(core.position);
| |
| innerGlow.scale.set(def.radius * def.innerGlowScale, def.radius * def.innerGlowScale, 1);
| |
| innerGlow.renderOrder = 2;
| |
|
| |
| outerGlowMaterial = new THREE.SpriteMaterial({
| |
| map: createSolarCoronaTexture(THREE),
| |
| color: 0xffffff,
| |
| transparent: true,
| |
| opacity: def.outerGlowOpacity,
| |
| depthWrite: false,
| |
| depthTest: true,
| |
| blending: THREE.AdditiveBlending
| |
| });
| |
|
| |
| outerGlow = new THREE.Sprite(outerGlowMaterial);
| |
| outerGlow.position.copy(core.position);
| |
| outerGlow.scale.set(def.radius * def.outerGlowScale, def.radius * def.outerGlowScale, 1);
| |
| outerGlow.renderOrder = 1;
| |
|
| |
| this.celestialGroup.add(outerGlow);
| |
| this.celestialGroup.add(innerGlow);
| |
| this.celestialGroup.add(core);
| |
|
| |
| this.celestialSun = core;
| |
| this.celestialSunGlowA = innerGlow;
| |
| this.celestialSunGlowB = outerGlow;
| |
| this.celestialSunCorona = null;
| |
| };
| |
|
| |
|
| |
| NationsGlobe.prototype.loadCelestialSun = function () {
| |
| var self = this;
| |
| var THREE = this.THREE;
| |
| var textureSize = Math.max(64, Math.min(1024, Math.floor(this.sunTextureSize || 512)));
| |
|
| |
| loadLimitedImageTexture(THREE, CELESTIAL_SUN_TEXTURE_URL, textureSize)
| |
| .then(function (texture) {
| |
| self.addCelestialSun(texture);
| |
| })
| |
| .catch(function (err) {
| |
| console.warn('CLBI sun texture failed:', err);
| |
| self.addCelestialSun(createFallbackPlanetTexture(THREE, '#d08b3e', '#5f240b'));
| |
| }); | | }); |
| };
| |
|
| |
|
| |
|
| |
| NationsGlobe.prototype.loadCelestialPlanets = function () {
| |
| var self = this;
| |
| var THREE = this.THREE;
| |
| var textureSize = Math.max(64, Math.min(1024, Math.floor(this.celestialTextureSize || 512)));
| |
|
| |
| if (!this.celestialEnabled || !this.celestialGroup) return;
| |
|
| |
| CELESTIAL_PLANET_DEFS.forEach(function (def) {
| |
| var url = CELESTIAL_PLANET_TEXTURES[def.key];
| |
|
| |
| if (!url) {
| |
| self.addCelestialPlanet(def, createFallbackPlanetTexture(THREE, def.fallbackA, def.fallbackB));
| |
| return;
| |
| }
| |
|
| |
| loadLimitedImageTexture(THREE, url, textureSize)
| |
| .then(function (texture) {
| |
| self.addCelestialPlanet(def, texture);
| |
| })
| |
| .catch(function (err) {
| |
| console.warn('CLBI celestial texture failed:', err);
| |
| self.addCelestialPlanet(def, createFallbackPlanetTexture(THREE, def.fallbackA, def.fallbackB));
| |
| });
| |
| });
| |
| };
| |
|
| |
| NationsGlobe.prototype.createCelestialBackdrop = function () {
| |
| var THREE = this.THREE;
| |
|
| |
| this.starGroup = new THREE.Group();
| |
| this.celestialGroup = new THREE.Group();
| |
|
| |
| this.scene.add(this.starGroup);
| |
| this.scene.add(this.celestialGroup);
| |
|
| |
| this.starGroup.add(createStars(THREE, 780));
| |
|
| |
| this.loadCelestialSun();
| |
| this.loadCelestialPlanets();
| |
| };
| |
|
| |
| NationsGlobe.prototype.updateCelestialBackdrop = function (dt, now) {
| |
| var i;
| |
| var planet;
| |
| var def = CELESTIAL_SUN_DEF;
| |
| var pulseA;
| |
| var pulseB;
| |
| var sunOccult = 0;
| |
|
| |
| if (!this.resetMotion) {
| |
| this.starDriftY += dt * this.starDriftSpeed;
| |
| this.celestialDriftY += dt * this.celestialDriftSpeed;
| |
| }
| |
|
| |
| if (this.starGroup) {
| |
| this.starGroup.rotation.x = -this.rotationX * this.starParallax;
| |
| this.starGroup.rotation.y = -this.rotationY * this.starParallax + this.starDriftY;
| |
| }
| |
|
| |
| if (this.celestialGroup) {
| |
| this.celestialGroup.rotation.x = -this.rotationX * this.celestialParallax;
| |
| this.celestialGroup.rotation.y = -this.rotationY * this.celestialParallax + this.celestialDriftY;
| |
| }
| |
|
| |
| for (i = 0; i < this.celestialPlanets.length; i += 1) {
| |
| planet = this.celestialPlanets[i];
| |
| if (planet && planet.mesh) {
| |
| planet.mesh.rotation.y += dt * planet.speed;
| |
| if (planet.shell) planet.shell.rotation.y += dt * planet.speed * 0.35;
| |
| if (planet.ring) planet.ring.rotation.z += dt * planet.speed * 0.10;
| |
| }
| |
| }
| |
|
| |
| if (this.celestialSun) {
| |
| this.celestialSun.rotation.y += dt * def.textureScrollSpeed;
| |
| if (this.celestialSun.material) {
| |
| this.celestialSun.material.opacity = def.coreOpacity * (1 - sunOccult);
| |
| }
| |
| }
| |
|
| |
| if (this.celestialSunGlowA) {
| |
| pulseA = def.radius * def.innerGlowScale * (1 + Math.sin(now * 0.00110) * def.pulseA);
| |
| this.celestialSunGlowA.scale.set(pulseA, pulseA, 1);
| |
| if (this.celestialSunGlowA.material) {
| |
| this.celestialSunGlowA.material.opacity = def.innerGlowOpacity * (1 + Math.sin(now * 0.00082 + 1.7) * (def.pulseB * 0.35)) * (1 - sunOccult);
| |
| }
| |
| }
| |
|
| |
| if (this.celestialSunGlowB) {
| |
| pulseB = def.radius * def.outerGlowScale * (1 + Math.sin(now * 0.00057 + 0.9) * def.pulseB);
| |
| this.celestialSunGlowB.scale.set(pulseB, pulseB, 1);
| |
| if (this.celestialSunGlowB.material) {
| |
| this.celestialSunGlowB.material.opacity = def.outerGlowOpacity * (1 + Math.sin(now * 0.00043 + 2.4) * (def.pulseA * 0.45)) * (1 - sunOccult);
| |
| }
| |
| }
| |
|
| |
| if (this.moon) {
| |
| this.moon.rotation.y += dt * CELESTIAL_MOON_DEF.rotationSpeed;
| |
| if (this.moonShell) this.moonShell.rotation.y += dt * CELESTIAL_MOON_DEF.rotationSpeed * 0.35;
| |
| }
| |
|
| |
| this.updateMoonOpacity(dt);
| |
| };
| |
|
| |
| NationsGlobe.prototype.createScene = function () {
| |
| var THREE = this.THREE;
| |
| var rect = this.stage.getBoundingClientRect();
| |
| var width = Math.max(320, Math.floor(rect.width || this.root.clientWidth || 960));
| |
| var height = Math.max(240, Math.floor(rect.height || this.root.clientHeight || 360));
| |
| var texture = createEarthTexture(THREE);
| |
| var atmosphere;
| |
| var light;
| |
| var ambient;
| |
|
| |
| this.scene = new THREE.Scene();
| |
| this.camera = new THREE.PerspectiveCamera(38, width / height, 0.1, 80);
| |
| this.camera.position.set(0, 0, this.cameraZ);
| |
|
| |
| this.renderer = new THREE.WebGLRenderer({
| |
| antialias: true,
| |
| alpha: true,
| |
| powerPreference: 'high-performance'
| |
| });
| |
| this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.7));
| |
| this.renderer.setSize(width, height, false);
| |
| this.renderer.setClearColor(0x000000, 0);
| |
| this.stage.appendChild(this.renderer.domElement);
| |
|
| |
| this.group = new THREE.Group();
| |
| this.scene.add(this.group);
| |
|
| |
| this.earthMaterial = new THREE.MeshPhongMaterial({
| |
| map: texture,
| |
| color: 0x9fb3bb,
| |
| emissive: 0x05080a,
| |
| shininess: 10,
| |
| transparent: false
| |
| });
| |
|
| |
| this.earth = new THREE.Mesh(
| |
| new THREE.SphereGeometry(this.radius, 96, 64),
| |
| this.earthMaterial
| |
| );
| |
| this.group.add(this.earth);
| |
|
| |
| atmosphere = new THREE.Mesh(
| |
| new THREE.SphereGeometry(this.radius * 1.035, 96, 64),
| |
| new THREE.MeshBasicMaterial({
| |
| color: 0x5f88a8,
| |
| transparent: true,
| |
| opacity: 0.115,
| |
| side: THREE.BackSide,
| |
| depthWrite: false
| |
| })
| |
| );
| |
| this.group.add(atmosphere);
| |
|
| |
| this.loadMoon();
| |
|
| |
| light = new THREE.DirectionalLight(0xffffff, 2.2);
| |
| light.position.set(3.2, 2.4, 4.8);
| |
| this.scene.add(light);
| |
|
| |
| ambient = new THREE.AmbientLight(0x607080, 0.66);
| |
| this.scene.add(ambient);
| |
|
| |
| this.createCelestialBackdrop();
| |
|
| |
| this.raycaster = new THREE.Raycaster();
| |
| this.pointer = new THREE.Vector2();
| |
|
| |
| this.applyRotation();
| |
| };
| |
|
| |
|
| |
| NationsGlobe.prototype.updateCloudOpacity = function () {
| |
| if (!this.cloudMaterial) return;
| |
|
| |
| var minZ = this.cameraZMin;
| |
| var maxZ = this.cameraZMax;
| |
| var t = clamp01((this.cameraZ - minZ) / (maxZ - minZ));
| |
| var nearOpacity = Math.max(0, Math.min(1, this.cloudOpacityNear));
| |
| var farOpacity = Math.max(0, Math.min(1, this.cloudOpacityFar));
| |
|
| |
| this.cloudMaterial.opacity = nearOpacity + (farOpacity - nearOpacity) * t;
| |
| this.cloudMaterial.needsUpdate = true;
| |
| };
| |
|
| |
|
| |
| NationsGlobe.prototype.getProjectedScreenRadius = function (object3D, radius) {
| |
| var THREE = this.THREE;
| |
| var center;
| |
| var edge;
| |
| var rect;
| |
| var cx;
| |
| var cy;
| |
| var ex;
| |
| var ey;
| |
|
| |
| if (!this.camera || !this.renderer || !object3D || !THREE) return null;
| |
|
| |
| center = new THREE.Vector3();
| |
| edge = new THREE.Vector3();
| |
|
| |
| object3D.getWorldPosition(center);
| |
| edge.copy(center).add(new THREE.Vector3(radius, 0, 0));
| |
|
| |
| center.project(this.camera);
| |
| edge.project(this.camera);
| |
|
| |
| rect = this.renderer.domElement.getBoundingClientRect();
| |
| cx = (center.x * 0.5 + 0.5) * rect.width;
| |
| cy = (-center.y * 0.5 + 0.5) * rect.height;
| |
| ex = (edge.x * 0.5 + 0.5) * rect.width;
| |
| ey = (-edge.y * 0.5 + 0.5) * rect.height;
| |
|
| |
| return {
| |
| x: cx,
| |
| y: cy,
| |
| r: Math.hypot(ex - cx, ey - cy)
| |
| };
| |
| };
| |
|
| |
| NationsGlobe.prototype.getMoonEarthOverlapFactor = function () {
| |
| var earthScreen;
| |
| var moonScreen;
| |
| var distance;
| |
| var fadeStart;
| |
| var fadeEnd;
| |
|
| |
| if (!this.moon || !this.camera || !this.renderer || !this.group) return 0;
| |
|
| |
| earthScreen = this.getProjectedScreenRadius(this.group, this.radius);
| |
| moonScreen = this.getProjectedScreenRadius(this.moon, CELESTIAL_MOON_DEF.radius);
| |
|
| |
| if (!earthScreen || !moonScreen) return 0;
| |
|
| |
| distance = Math.hypot(moonScreen.x - earthScreen.x, moonScreen.y - earthScreen.y);
| |
| fadeStart = earthScreen.r + moonScreen.r + this.moonEarthFadeMargin;
| |
| fadeEnd = Math.max(0, earthScreen.r - moonScreen.r);
| |
|
| |
| if (distance >= fadeStart) return 0;
| |
| if (distance <= fadeEnd) return 1;
| |
|
| |
| return clamp01(1 - inverseLerp(fadeEnd, fadeStart, distance));
| |
| };
| |
|
| |
| NationsGlobe.prototype.getMoonSunOccultFactor = function () {
| |
| var THREE = this.THREE;
| |
| var moonScreen;
| |
| var sunScreen;
| |
| var moonWorld;
| |
| var sunWorld;
| |
| var moonCamera;
| |
| var sunCamera;
| |
| var distance;
| |
| var fadeStart;
| |
| var fadeEnd;
| |
| var geometryOccult;
| |
| var moonOpacityFactor;
| |
|
| |
| if (!this.moon || !this.celestialSun || !this.camera || !this.renderer || !THREE) return 0;
| |
| moonOpacityFactor = Math.max(0, Math.min(1, this.moonRenderedOpacity == null ? 1 : this.moonRenderedOpacity));
| |
|
| |
| /*
| |
| * A faded moon is a weaker occluder, but it must not suddenly release the sun.
| |
| * The final reveal is smoothed in updateSunOccultFactor.
| |
| */
| |
|
| |
| moonWorld = new THREE.Vector3();
| |
| sunWorld = new THREE.Vector3();
| |
|
| |
| this.moon.getWorldPosition(moonWorld);
| |
| this.celestialSun.getWorldPosition(sunWorld);
| |
|
| |
| moonCamera = moonWorld.clone().applyMatrix4(this.camera.matrixWorldInverse);
| |
| sunCamera = sunWorld.clone().applyMatrix4(this.camera.matrixWorldInverse);
| |
|
| |
| /*
| |
| * In camera space, objects in front of the camera have negative z values.
| |
| * The less negative value is closer. Only the closer moon can occult the sun.
| |
| */
| |
| if (moonCamera.z <= sunCamera.z) return 0;
| |
|
| |
| moonScreen = this.getProjectedScreenRadius(this.moon, CELESTIAL_MOON_DEF.radius);
| |
| sunScreen = this.getProjectedScreenRadius(this.celestialSun, CELESTIAL_SUN_DEF.radius * 1.18);
| |
|
| |
| if (!moonScreen || !sunScreen) return 0;
| |
|
| |
| distance = Math.hypot(moonScreen.x - sunScreen.x, moonScreen.y - sunScreen.y);
| |
| fadeStart = moonScreen.r + sunScreen.r;
| |
| fadeEnd = Math.max(0, moonScreen.r - sunScreen.r * 0.35);
| |
|
| |
| if (distance >= fadeStart) return 0;
| |
|
| |
| geometryOccult = distance <= fadeEnd
| |
| ? 1
| |
| : clamp01(1 - inverseLerp(fadeEnd, fadeStart, distance));
| |
|
| |
| return geometryOccult * moonOpacityFactor;
| |
| };
| |
|
| |
| NationsGlobe.prototype.updateSunOccultFactor = function (targetOccult, dt) {
| |
| var target = Math.max(0, Math.min(1, targetOccult || 0));
| |
| var duration = Math.max(120, this.sunOccultFadeDuration || this.moonFadeDuration || this.resetDuration || 520);
| |
| var step = dt == null ? 16.6667 : Math.max(0, Math.min(80, dt));
| |
| var alpha = 1 - Math.pow(1 - clamp01(step / duration), 3);
| |
|
| |
| /*
| |
| * Do not create a new transition every time the target changes.
| |
| * The moon is fading every frame, so targetOccult changes every frame too.
| |
| * A continuous low-pass interpolation prevents the sun from popping out
| |
| * when the moon finally stops acting as an occluder.
| |
| */
| |
| this.sunOccultRendered = (this.sunOccultRendered || 0) + (target - (this.sunOccultRendered || 0)) * alpha;
| |
|
| |
| if (Math.abs(this.sunOccultRendered - target) < 0.0008) {
| |
| this.sunOccultRendered = target;
| |
| }
| |
|
| |
| if (this.sunOccultRendered < 0.0008) this.sunOccultRendered = 0;
| |
| if (this.sunOccultRendered > 0.9992) this.sunOccultRendered = 1;
| |
|
| |
| return this.sunOccultRendered;
| |
| };
| |
|
| |
| NationsGlobe.prototype.updateMoonOpacity = function (dt) {
| |
| var vanishZ = Math.max(this.cameraZMin, this.moonVanishZ);
| |
| var fullZ = Math.max(vanishZ + 0.01, this.moonFullVisibleZ);
| |
| var t = clamp01((this.cameraZ - vanishZ) / (fullZ - vanishZ));
| |
| var moonNear = Math.max(0, Math.min(1, this.moonOpacityNear));
| |
| var moonFar = Math.max(0, Math.min(1, this.moonOpacityFar));
| |
| var shellNear = Math.max(0, Math.min(1, this.moonShellOpacityNear));
| |
| var shellFar = Math.max(0, Math.min(1, this.moonShellOpacityFar));
| |
| var earthFadeOpacity = Math.max(0, Math.min(1, this.moonEarthFadeOpacity));
| |
| var overlap = this.getMoonEarthOverlapFactor();
| |
| var targetOpacity;
| |
| var targetShellOpacity;
| |
| var duration = Math.max(120, this.moonFadeDuration || this.resetDuration || 520);
| |
| var step = dt == null ? 16.6667 : Math.max(0, Math.min(80, dt));
| |
| var transition;
| |
| var k;
| |
| var isSolidMoon;
| |
|
| |
| if (this.cameraZ <= vanishZ) {
| |
| targetOpacity = 0;
| |
| targetShellOpacity = 0;
| |
| } else {
| |
| targetOpacity = moonNear + (moonFar - moonNear) * t;
| |
| targetShellOpacity = shellNear + (shellFar - shellNear) * t;
| |
|
| |
|
| if (overlap > 0) { | | var initial = panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent]') || tabs[0]; |
| targetOpacity = targetOpacity + (earthFadeOpacity - targetOpacity) * overlap;
| | if (initial) activate(initial.getAttribute('data-continent')); |
| targetShellOpacity = targetShellOpacity * (1 - overlap);
| |
| }
| |
| } | | } |
|
| |
|
| if (!this.moonFadeMotion || | | function init(root) { |
| Math.abs((this.moonFadeMotion.targetOpacity || 0) - targetOpacity) > 0.001 ||
| | var scope = root && root.querySelectorAll ? root : document; |
| Math.abs((this.moonFadeMotion.targetShellOpacity || 0) - targetShellOpacity) > 0.001) {
| | Array.prototype.forEach.call(scope.querySelectorAll('.clbi-nations-tabpanel'), initPanel); |
| | |
| this.moonFadeMotion = { | |
| elapsed: 0,
| |
| duration: duration,
| |
| fromOpacity: this.moonRenderedOpacity === null ? targetOpacity : this.moonRenderedOpacity,
| |
| fromShellOpacity: this.moonRenderedShellOpacity === null ? targetShellOpacity : this.moonRenderedShellOpacity,
| |
| targetOpacity: targetOpacity,
| |
| targetShellOpacity: targetShellOpacity
| |
| };
| |
| }
| |
| | |
| transition = this.moonFadeMotion;
| |
| | |
| if (transition) {
| |
| transition.elapsed += step; | |
| k = easeOutCubic(clamp01(transition.elapsed / transition.duration));
| |
| | |
| this.moonRenderedOpacity = transition.fromOpacity + (transition.targetOpacity - transition.fromOpacity) * k;
| |
| this.moonRenderedShellOpacity = transition.fromShellOpacity + (transition.targetShellOpacity - transition.fromShellOpacity) * k;
| |
| | |
| if (transition.elapsed >= transition.duration) {
| |
| this.moonRenderedOpacity = transition.targetOpacity;
| |
| this.moonRenderedShellOpacity = transition.targetShellOpacity;
| |
| this.moonFadeMotion = null;
| |
| }
| |
| } else {
| |
| this.moonRenderedOpacity = targetOpacity;
| |
| this.moonRenderedShellOpacity = targetShellOpacity;
| |
| } | | } |
|
| |
|
| /* | | window.CLBI_NATIONS_PANEL = { |
| * Do not hide at a visible 1% tail. Keep rendering until the opacity is
| | init: init |
| * effectively zero, so the final disappearance reads as a fade, not a pop.
| |
| */
| |
| if (this.moonRenderedOpacity < 0.0008) this.moonRenderedOpacity = 0;
| |
| if (this.moonRenderedShellOpacity < 0.0008) this.moonRenderedShellOpacity = 0;
| |
| | |
| isSolidMoon = overlap <= 0.001 && this.moonRenderedOpacity >= 0.999;
| |
| | |
| if (this.moon && this.moon.material) {
| |
| this.moon.material.transparent = !isSolidMoon;
| |
| this.moon.material.opacity = isSolidMoon ? 1 : this.moonRenderedOpacity;
| |
| this.moon.material.depthWrite = true;
| |
| this.moon.visible = (isSolidMoon ? 1 : this.moonRenderedOpacity) > 0;
| |
| this.moon.material.needsUpdate = true;
| |
| }
| |
| | |
| if (this.moonShell && this.moonShell.material) {
| |
| this.moonShell.material.opacity = this.moonRenderedShellOpacity;
| |
| this.moonShell.visible = this.moonRenderedShellOpacity > 0;
| |
| this.moonShell.material.needsUpdate = true;
| |
| }
| |
| };
| |
| | |
| | |
| NationsGlobe.prototype.loadOptionalTextures = function () {
| |
| var self = this;
| |
| var THREE = this.THREE;
| |
| var tasks = [];
| |
| | |
| if (this.terrainUrl && this.earthMaterial) {
| |
| var earthLayers = [];
| |
| | |
| if (this.reliefOverlayUrl) {
| |
| earthLayers.push({
| |
| label: 'relief',
| |
| url: this.reliefOverlayUrl,
| |
| opacity: this.reliefOverlayOpacity,
| |
| blend: this.reliefOverlayBlend || 'source-over',
| |
| filter: this.reliefOverlayFilter || 'none'
| |
| });
| |
| }
| |
| | |
| if (this.polygonOverlayEnabled && this.polygonOverlayUrl) {
| |
| earthLayers.push({
| |
| label: 'polygon',
| |
| url: this.polygonOverlayUrl,
| |
| opacity: this.polygonOverlayOpacity,
| |
| blend: this.polygonOverlayBlend || 'source-over',
| |
| filter: this.polygonOverlayFilter || 'none'
| |
| });
| |
| }
| |
| | |
| if (earthLayers.length) {
| |
| tasks.push(
| |
| createCompositedEarthTextureLayers(THREE, this.terrainUrl, earthLayers, this.earthCompositeSize)
| |
| .then(function (texture) {
| |
| if (!self.earthMaterial) return;
| |
| self.earthMaterial.map = texture;
| |
| self.earthMaterial.color.set(0xffffff);
| |
| self.earthMaterial.emissive.set(0x020304);
| |
| self.earthMaterial.shininess = 6;
| |
| self.earthMaterial.needsUpdate = true;
| |
| | |
| if (window.console && console.info) {
| |
| console.info('CLBI earth composite applied:', {
| |
| terrainUrl: self.terrainUrl,
| |
| earthCompositeSize: self.earthCompositeSize,
| |
| reliefOverlayUrl: self.reliefOverlayUrl,
| |
| reliefOverlayOpacity: self.reliefOverlayOpacity,
| |
| reliefOverlayBlend: self.reliefOverlayBlend,
| |
| reliefOverlayFilter: self.reliefOverlayFilter,
| |
| polygonOverlayUrl: self.polygonOverlayUrl,
| |
| polygonOverlayOpacity: self.polygonOverlayOpacity,
| |
| polygonOverlayBlend: self.polygonOverlayBlend,
| |
| polygonOverlayFilter: self.polygonOverlayFilter
| |
| });
| |
| }
| |
| })
| |
| .catch(function (err) {
| |
| console.warn('CLBI earth composite failed, falling back to base terrain:', err);
| |
| return loadImageTexture(THREE, self.terrainUrl)
| |
| .then(function (texture) {
| |
| if (!self.earthMaterial) return;
| |
| self.earthMaterial.map = texture;
| |
| self.earthMaterial.color.set(0xffffff);
| |
| self.earthMaterial.emissive.set(0x020304);
| |
| self.earthMaterial.shininess = 6;
| |
| self.earthMaterial.needsUpdate = true;
| |
| });
| |
| })
| |
| );
| |
| } else {
| |
| tasks.push(
| |
| loadImageTexture(THREE, this.terrainUrl)
| |
| .then(function (texture) {
| |
| if (!self.earthMaterial) return;
| |
| self.earthMaterial.map = texture;
| |
| self.earthMaterial.color.set(0xffffff);
| |
| self.earthMaterial.emissive.set(0x020304);
| |
| self.earthMaterial.shininess = 6;
| |
| self.earthMaterial.needsUpdate = true;
| |
| })
| |
| .catch(function (err) {
| |
| console.warn('CLBI terrain texture failed:', err);
| |
| })
| |
| );
| |
| }
| |
| }
| |
| | |
| if (this.lakesOverlayUrl && this.group) {
| |
| tasks.push( | |
| loadImageTexture(THREE, this.lakesOverlayUrl)
| |
| .then(function (texture) {
| |
| var material;
| |
| var mesh;
| |
| var radius;
| |
| | |
| if (!self.group) return;
| |
| | |
| radius = self.radius + Math.max(0.0012, Math.min(0.008, self.lakesSurfaceOffset));
| |
| | |
| material = new THREE.MeshBasicMaterial({
| |
| map: texture,
| |
| transparent: true,
| |
| opacity: Math.max(0, Math.min(1.25, self.lakesOverlayOpacity)),
| |
| depthWrite: false,
| |
| depthTest: true
| |
| });
| |
| | |
| mesh = new THREE.Mesh(
| |
| new THREE.SphereGeometry(radius, 128, 64),
| |
| material
| |
| );
| |
| mesh.renderOrder = 12;
| |
| self.group.add(mesh);
| |
| self.lakesOverlay = mesh;
| |
| | |
| if (window.console && console.info) {
| |
| console.info('CLBI 1950 lakes overlay applied:', {
| |
| lakesOverlayUrl: self.lakesOverlayUrl,
| |
| lakesOverlayOpacity: self.lakesOverlayOpacity,
| |
| lakesSurfaceOffset: self.lakesSurfaceOffset
| |
| });
| |
| }
| |
| })
| |
| .catch(function (err) {
| |
| console.warn('CLBI lakes overlay failed:', err);
| |
| })
| |
| );
| |
| }
| |
| | |
| if (this.cloudsUrl) {
| |
| tasks.push(
| |
| loadImageTexture(THREE, this.cloudsUrl)
| |
| .then(function (texture) {
| |
| var material = new THREE.MeshBasicMaterial({
| |
| map: texture,
| |
| color: 0xffffff,
| |
| transparent: true,
| |
| opacity: self.cloudOpacityFar,
| |
| depthWrite: false,
| |
| side: THREE.DoubleSide
| |
| });
| |
| | |
| self.cloudMaterial = material;
| |
| self.clouds = new THREE.Mesh(
| |
| new THREE.SphereGeometry(self.radius * 1.018, 96, 64),
| |
| material
| |
| );
| |
| self.clouds.renderOrder = 4;
| |
| self.group.add(self.clouds);
| |
| self.updateCloudOpacity();
| |
| })
| |
| .catch(function (err) {
| |
| console.warn('CLBI cloud texture failed:', err);
| |
| })
| |
| );
| |
| }
| |
| | |
| return Promise.all(tasks).then(function () {});
| |
| };
| |
| | |
| | |
| NationsGlobe.prototype.cancelResetMotion = function () {
| |
| this.resetMotion = null;
| |
| this.autoResumeMotion = null;
| |
| };
| |
| | |
| NationsGlobe.prototype.startAutoResumeMotion = function () {
| |
| if (!this.autoRotate) {
| |
| this.autoResumeMotion = null;
| |
| return;
| |
| }
| |
| | |
| this.autoResumeMotion = {
| |
| elapsed: 0,
| |
| duration: Math.max(900, this.autoResumeDuration || this.resetDuration || 1800),
| |
| from: Math.max(0, Math.min(0.35, this.autoResumeMinFactor || 0.06)),
| |
| to: 1
| |
| };
| |
| };
| |
| | |
| NationsGlobe.prototype.getAutoRotateFactor = function (dt) {
| |
| var motion = this.autoResumeMotion;
| |
| var t;
| |
| | |
| if (!motion) return 1;
| |
| | |
| motion.elapsed += dt;
| |
| t = clamp01(motion.elapsed / motion.duration);
| |
| | |
| /*
| |
| * Speedometer-style ramp: no hidden threshold, no late kick.
| |
| * The factor rises almost linearly from a very slow crawl to normal speed,
| |
| * so the last frame before completion is already nearly identical to 1.0.
| |
| */
| |
| if (t >= 1) {
| |
| this.autoResumeMotion = null;
| |
| return 1;
| |
| }
| |
| | |
| return motion.from + (motion.to - motion.from) * t;
| |
| };
| |
| | |
| NationsGlobe.prototype.startResetMotion = function () {
| |
| var toY;
| |
| | |
| this.velocityX = 0;
| |
| this.velocityY = 0;
| |
| this.dragging = false;
| |
| this.autoResumeMotion = null;
| |
| this.autoRotate = this.resetAutoRotate;
| |
| this.syncAutoButton();
| |
| this.root.classList.remove('is-dragging');
| |
| | |
| /*
| |
| * RESET restores the exact first-load state.
| |
| * The earth still takes the nearest visual path back, but the sky drift is
| |
| * compensated during the animation so the celestial background lands on the
| |
| * same phase it had when the globe first opened.
| |
| */
| |
| toY = nearestEquivalentAngle(this.rotationY, this.resetRotationY);
| |
| | |
| this.resetMotion = {
| |
| elapsed: 0,
| |
| duration: Math.max(120, this.resetDuration),
| |
| fromX: this.rotationX,
| |
| fromY: this.rotationY,
| |
| fromZ: this.cameraZ,
| |
| fromStarDriftY: this.starDriftY,
| |
| fromCelestialDriftY: this.celestialDriftY,
| |
| toX: this.resetRotationX,
| |
| toY: toY,
| |
| finalY: this.resetRotationY,
| |
| toZ: this.resetCameraZ,
| |
| toStarDriftY: (toY - this.resetRotationY) * this.starParallax,
| |
| toCelestialDriftY: (toY - this.resetRotationY) * this.celestialParallax
| |
| }; | | }; |
| };
| |
|
| |
| NationsGlobe.prototype.stepResetMotion = function (dt) {
| |
| var motion = this.resetMotion;
| |
| var t;
| |
| var k;
| |
|
| |
| if (!motion) return false;
| |
|
| |
| motion.elapsed += dt;
| |
| t = clamp01(motion.elapsed / motion.duration);
| |
| k = easeOutCubic(t);
| |
|
| |
| this.rotationX = motion.fromX + (motion.toX - motion.fromX) * k;
| |
| this.rotationY = motion.fromY + (motion.toY - motion.fromY) * k;
| |
| this.cameraZ = motion.fromZ + (motion.toZ - motion.fromZ) * k;
| |
| this.starDriftY = motion.fromStarDriftY + (motion.toStarDriftY - motion.fromStarDriftY) * k;
| |
| this.celestialDriftY = motion.fromCelestialDriftY + (motion.toCelestialDriftY - motion.fromCelestialDriftY) * k;
| |
|
| |
| if (this.camera) this.camera.position.z = this.cameraZ;
| |
| this.updateCloudOpacity();
| |
| this.updateMoonOpacity(dt);
| |
| this.applyRotation();
| |
|
| |
| if (t >= 1) {
| |
| this.rotationX = motion.toX;
| |
| this.rotationY = motion.finalY;
| |
| this.cameraZ = motion.toZ;
| |
| this.starDriftY = 0;
| |
| this.celestialDriftY = 0;
| |
| if (this.camera) this.camera.position.z = this.cameraZ;
| |
| this.updateCloudOpacity();
| |
| this.applyRotation();
| |
| this.resetMotion = null;
| |
| if (this.autoRotate) this.startAutoResumeMotion();
| |
| }
| |
|
| |
| return true;
| |
| };
| |
|
| |
| NationsGlobe.prototype.applyRotation = function () {
| |
| if (!this.group) return;
| |
| this.rotationX = Math.max(-1.15, Math.min(1.15, this.rotationX));
| |
| this.group.rotation.x = this.rotationX;
| |
| this.group.rotation.y = this.rotationY;
| |
| };
| |
|
| |
| NationsGlobe.prototype.resize = function () {
| |
| if (!this.renderer || !this.camera || !this.stage) return;
| |
|
| |
| var rect = this.stage.getBoundingClientRect();
| |
| var width = Math.max(320, Math.floor(rect.width || 0));
| |
| var height = Math.max(240, Math.floor(rect.height || 0));
| |
|
| |
| if (!width || !height) return;
| |
|
| |
| this.camera.aspect = width / height;
| |
| this.camera.updateProjectionMatrix();
| |
| this.renderer.setSize(width, height, false);
| |
| };
| |
|
| |
|
| |
| NationsGlobe.prototype.syncAutoButton = function () {
| |
| if (this.autoButton) {
| |
| this.autoButton.classList.toggle('is-active', this.autoRotate);
| |
| }
| |
| };
| |
|
| |
| NationsGlobe.prototype.bindControlEvents = function () {
| |
| var self = this;
| |
|
| |
| if (this.autoButton && this.autoButton.getAttribute('data-clbi-globe-control-ready') !== '1') {
| |
| this.autoButton.setAttribute('data-clbi-globe-control-ready', '1');
| |
| this.autoButton.addEventListener('click', function (e) {
| |
| self.cancelResetMotion();
| |
| self.autoResumeMotion = null;
| |
| self.autoRotate = !self.autoRotate;
| |
| self.velocityX = 0;
| |
| self.velocityY = 0;
| |
| self.syncAutoButton();
| |
| e.preventDefault();
| |
| e.stopPropagation();
| |
| });
| |
| }
| |
|
| |
|
| if (this.resetButton && this.resetButton.getAttribute('data-clbi-globe-control-ready') !== '1') { | | if (window.mw && mw.hook) { |
| this.resetButton.setAttribute('data-clbi-globe-control-ready', '1'); | | mw.hook('wikipage.content').add(function ($content) { |
| this.resetButton.addEventListener('click', function (e) {
| | init($content && $content[0] ? $content[0] : document); |
| self.startResetMotion(); | |
| e.preventDefault();
| |
| e.stopPropagation();
| |
| }); | | }); |
| }
| |
| };
| |
|
| |
|
| |
| NationsGlobe.prototype.trackPointerPosition = function (e) {
| |
| if (!e || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return;
| |
|
| |
| this.pointerInside = true;
| |
| this.pointerClientX = e.clientX;
| |
| this.pointerClientY = e.clientY;
| |
| };
| |
|
| |
| NationsGlobe.prototype.clearPointerPosition = function () {
| |
| this.pointerInside = false;
| |
| this.pointerClientX = null;
| |
| this.pointerClientY = null;
| |
|
| |
| this.clearNationHover();
| |
|
| |
| if (this.readout) {
| |
| this.setReadout(this.getBaseReadout());
| |
| }
| |
| };
| |
|
| |
| NationsGlobe.prototype.getBaseReadout = function () {
| |
| return (this.root.getAttribute('data-era') || 'REAL WORLD / 110M') + ' · SEG ' + this.segmentCount;
| |
| };
| |
|
| |
| NationsGlobe.prototype.bindEvents = function () {
| |
| var self = this;
| |
|
| |
| if (this.stageEventsBound) {
| |
| this.bindControlEvents();
| |
| return;
| |
| }
| |
|
| |
| this.stageEventsBound = true;
| |
|
| |
| this.stage.addEventListener('pointerenter', function (e) {
| |
| self.trackPointerPosition(e);
| |
| self.updatePointerReadout(e);
| |
| });
| |
|
| |
| this.stage.addEventListener('pointerleave', function () {
| |
| if (!self.dragging) {
| |
| self.clearPointerPosition();
| |
| }
| |
| });
| |
|
| |
| this.stage.addEventListener('pointerdown', function (e) {
| |
| self.trackPointerPosition(e);
| |
| self.updatePointerReadout(e);
| |
| self.cancelResetMotion();
| |
| self.dragging = true;
| |
| self.pointerDownX = e.clientX;
| |
| self.pointerDownY = e.clientY;
| |
| self.pointerDownNationPage = self.currentNation ? self.currentNation.page : null;
| |
| self.lastX = e.clientX;
| |
| self.lastY = e.clientY;
| |
| self.lastMoveTime = e.timeStamp || performance.now();
| |
| self.velocityX = 0;
| |
| self.velocityY = 0;
| |
| self.root.classList.add('is-dragging');
| |
| self.autoResumeMotion = null;
| |
| self.autoRotate = false;
| |
| self.syncAutoButton();
| |
|
| |
| if (self.stage.setPointerCapture && e.pointerId != null) {
| |
| try { self.stage.setPointerCapture(e.pointerId); } catch (err) {}
| |
| }
| |
|
| |
| e.preventDefault();
| |
| });
| |
|
| |
| this.stage.addEventListener('pointermove', function (e) {
| |
| self.trackPointerPosition(e);
| |
|
| |
| if (self.dragging) {
| |
| var dx = e.clientX - self.lastX;
| |
| var dy = e.clientY - self.lastY;
| |
| var now = e.timeStamp || performance.now();
| |
| var elapsed = Math.max(8, Math.min(80, now - (self.lastMoveTime || now || performance.now())));
| |
| var stepY = dx * self.dragSensitivityY;
| |
| var stepX = dy * self.dragSensitivityX;
| |
| var instantY = (stepY / elapsed) * self.inertiaStrength;
| |
| var instantX = (stepX / elapsed) * self.inertiaStrength;
| |
|
| |
| self.rotationY += stepY;
| |
| self.rotationX += stepX;
| |
| self.velocityY = self.velocityY * 0.58 + instantY * 0.42;
| |
| self.velocityX = self.velocityX * 0.58 + instantX * 0.42;
| |
| self.lastX = e.clientX;
| |
| self.lastY = e.clientY;
| |
| self.lastMoveTime = now;
| |
| self.applyRotation();
| |
| self.updatePointerReadout(e);
| |
| e.preventDefault();
| |
| } else {
| |
| self.updatePointerReadout(e);
| |
| }
| |
| });
| |
|
| |
| function finishDrag(e) {
| |
| var now;
| |
| var releaseDelay;
| |
| var releaseSpeed;
| |
|
| |
| if (!self.dragging) return;
| |
|
| |
| now = e && e.timeStamp ? e.timeStamp : performance.now();
| |
| releaseDelay = now - (self.lastMoveTime || now);
| |
| releaseSpeed = Math.max(Math.abs(self.velocityX), Math.abs(self.velocityY));
| |
|
| |
| /*
| |
| * Only a deliberate throw should keep spinning.
| |
| * If the user drags, holds, adjusts the viewing angle, and then releases,
| |
| * the last movement is old or slow, so the globe must settle immediately.
| |
| */
| |
| if (releaseDelay > self.inertiaReleaseWindow || releaseSpeed < self.inertiaMinVelocity) {
| |
| self.velocityX = 0;
| |
| self.velocityY = 0;
| |
| }
| |
|
| |
| if (e && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
| |
| self.trackPointerPosition(e);
| |
| self.updatePointerReadout(e);
| |
| }
| |
|
| |
| self.dragging = false;
| |
| self.root.classList.remove('is-dragging');
| |
| if (self.stage.releasePointerCapture && e && e.pointerId != null) {
| |
| try { self.stage.releasePointerCapture(e.pointerId); } catch (err) {}
| |
| }
| |
|
| |
| self.handleNationClick(e);
| |
| }
| |
|
| |
| this.stage.addEventListener('pointerup', finishDrag);
| |
| this.stage.addEventListener('pointercancel', finishDrag);
| |
|
| |
| this.stage.addEventListener('wheel', function (e) {
| |
| self.trackPointerPosition(e);
| |
| self.cancelResetMotion();
| |
| self.velocityX = 0;
| |
| self.velocityY = 0;
| |
| self.cameraZ += e.deltaY * 0.0017;
| |
| self.cameraZ = Math.max(self.cameraZMin, Math.min(self.cameraZMax, self.cameraZ));
| |
| if (self.camera) self.camera.position.z = self.cameraZ;
| |
| self.updateCloudOpacity();
| |
| self.updateMoonOpacity();
| |
| self.updatePointerReadout(e);
| |
| e.preventDefault();
| |
| }, { passive: false });
| |
|
| |
| this.bindControlEvents();
| |
|
| |
| window.addEventListener('resize', function () {
| |
| self.resize();
| |
| });
| |
| };
| |
|
| |
| NationsGlobe.prototype.updatePointerReadout = function (e) {
| |
| if (e && typeof e.clientX === 'number' && typeof e.clientY === 'number') {
| |
| this.trackPointerPosition(e);
| |
| }
| |
|
| |
| if (this.pointerClientX === null || this.pointerClientY === null) return false;
| |
|
| |
| return this.updatePointerReadoutFromClient(this.pointerClientX, this.pointerClientY);
| |
| };
| |
|
| |
| NationsGlobe.prototype.updatePointerReadoutFromClient = function (clientX, clientY) {
| |
| var THREE = this.THREE;
| |
| var rect;
| |
| var intersects;
| |
| var local;
| |
| var ll;
| |
|
| |
| if (!this.raycaster || !this.pointer || !this.camera || !this.earth || !this.renderer) return false;
| |
|
| |
| rect = this.renderer.domElement.getBoundingClientRect();
| |
|
| |
| if (!rect.width || !rect.height) return false;
| |
|
| |
| if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {
| |
| this.setReadout(this.getBaseReadout());
| |
| return false;
| |
| }
| |
|
| |
| this.pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
| |
| this.pointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;
| |
|
| |
| this.raycaster.setFromCamera(this.pointer, this.camera);
| |
| intersects = this.raycaster.intersectObject(this.earth, false);
| |
|
| |
| if (intersects && intersects.length) {
| |
| local = this.earth.worldToLocal(intersects[0].point.clone());
| |
| ll = vectorToLonLat(local);
| |
| this.updateNationHover(ll, clientX, clientY);
| |
| if (this.currentNation) {
| |
| this.setReadout(this.getNationLabel(this.currentNation.page) + ' · LAT ' + ll.lat.toFixed(2) + ' / LON ' + ll.lon.toFixed(2) + ' · SEG ' + this.segmentCount);
| |
| } else {
| |
| this.setReadout('LAT ' + ll.lat.toFixed(2) + ' / LON ' + ll.lon.toFixed(2) + ' · SEG ' + this.segmentCount);
| |
| }
| |
| return true;
| |
| }
| |
|
| |
| this.clearNationHover();
| |
| this.setReadout(this.getBaseReadout());
| |
| return false;
| |
| };
| |
|
| |
|
| |
|
| |
| NationsGlobe.prototype.loadNationInteractionData = function () {
| |
| var self = this;
| |
| var tasks = [];
| |
|
| |
| if (this.nationHitUrl) {
| |
| tasks.push(
| |
| fetch(this.nationHitUrl, { credentials: 'same-origin' })
| |
| .then(function (res) {
| |
| if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + self.nationHitUrl);
| |
| return res.json();
| |
| })
| |
| .then(function (payload) {
| |
| var features = payload && payload.features ? payload.features : [];
| |
| self.nationHitFeatures = features
| |
| .filter(function (feature) {
| |
| return getFeaturePage(feature) && feature.geometry;
| |
| })
| |
| .map(function (feature) {
| |
| var bbox = feature.bbox || calculateGeometryBBox(feature.geometry);
| |
| return {
| |
| page: getFeaturePage(feature),
| |
| bbox: bbox,
| |
| bboxArea: Math.max(0, (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])),
| |
| geometry: feature.geometry
| |
| };
| |
| })
| |
| .sort(function (a, b) {
| |
| return a.bboxArea - b.bboxArea;
| |
| });
| |
|
| |
| if (window.console && console.info) {
| |
| console.info('1950 nation hit data applied:', {
| |
| url: self.nationHitUrl,
| |
| features: self.nationHitFeatures.length
| |
| });
| |
| }
| |
| })
| |
| .catch(function (err) {
| |
| self.nationHitFeatures = [];
| |
| if (window.console && console.warn) console.warn('1950 nation hit data failed:', err);
| |
| })
| |
| );
| |
| }
| |
|
| |
| if (this.nationLinkMapUrl) {
| |
| tasks.push(
| |
| fetch(this.nationLinkMapUrl, { credentials: 'same-origin', cache: 'no-cache' })
| |
| .then(function (res) {
| |
| if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + self.nationLinkMapUrl);
| |
| return res.json();
| |
| })
| |
| .then(function (payload) {
| |
| self.nationLinkMap = normalizeNationLinkMap(payload);
| |
| if (window.console && console.info) {
| |
| console.info('1950 nation link map applied:', {
| |
| url: self.nationLinkMapUrl,
| |
| items: Object.keys(self.nationLinkMap).length
| |
| });
| |
| }
| |
| })
| |
| .catch(function (err) {
| |
| self.nationLinkMap = {};
| |
| if (window.console && console.warn) console.warn('1950 nation link map failed:', err);
| |
| })
| |
| );
| |
| }
| |
|
| |
| if (!tasks.length) return Promise.resolve();
| |
| return Promise.all(tasks).then(function () {});
| |
| };
| |
|
| |
| NationsGlobe.prototype.findNationAtLonLat = function (ll) {
| |
| var lon = ll && Number(ll.lon);
| |
| var lat = ll && Number(ll.lat);
| |
| var i;
| |
| var feature;
| |
|
| |
| if (!Number.isFinite(lon) || !Number.isFinite(lat) || !this.nationHitFeatures.length) return null;
| |
|
| |
| for (i = 0; i < this.nationHitFeatures.length; i += 1) {
| |
| feature = this.nationHitFeatures[i];
| |
| if (!isLonLatInsideBbox(lon, lat, feature.bbox)) continue;
| |
| if (pointInGeometry(lon, lat, feature.geometry)) {
| |
| return {
| |
| page: feature.page,
| |
| feature: feature
| |
| };
| |
| }
| |
| }
| |
|
| |
| return null;
| |
| };
| |
|
| |
| NationsGlobe.prototype.getNationEntry = function (page) {
| |
| return page && this.nationLinkMap ? this.nationLinkMap[page] || null : null;
| |
| };
| |
|
| |
| NationsGlobe.prototype.getNationLabel = function (page) {
| |
| var entry = this.getNationEntry(page);
| |
| return (entry && (entry.name_ko || entry.name || entry.wiki_title)) || page || '';
| |
| };
| |
|
| |
| NationsGlobe.prototype.getNationUrl = function (page) {
| |
| var entry = this.getNationEntry(page);
| |
| var title;
| |
|
| |
| if (!entry || entry.enabled === false) return '';
| |
| title = entry.wiki_title || entry.title || '';
| |
| if (!title) return '';
| |
| if (/^https?:\/\//i.test(title)) return title;
| |
| return this.nationLinkBase + encodeWikiTitle(title);
| |
| };
| |
|
| |
| NationsGlobe.prototype.positionNationTooltip = function (clientX, clientY) {
| |
| var rootRect;
| |
| var tooltipRect;
| |
| var left;
| |
| var top;
| |
|
| |
| if (!this.nationTooltip) return;
| |
|
| |
| rootRect = this.root.getBoundingClientRect();
| |
| left = clientX - rootRect.left + 14;
| |
| top = clientY - rootRect.top + 14;
| |
|
| |
| this.nationTooltip.style.left = left + 'px';
| |
| this.nationTooltip.style.top = top + 'px';
| |
|
| |
| tooltipRect = this.nationTooltip.getBoundingClientRect();
| |
| if (tooltipRect.right > rootRect.right - 6) {
| |
| left = Math.max(6, clientX - rootRect.left - tooltipRect.width - 12);
| |
| }
| |
| if (tooltipRect.bottom > rootRect.bottom - 6) {
| |
| top = Math.max(6, clientY - rootRect.top - tooltipRect.height - 12);
| |
| } | | } |
|
| |
|
| this.nationTooltip.style.left = left + 'px';
| | if (document.readyState === 'loading') { |
| this.nationTooltip.style.top = top + 'px';
| | document.addEventListener('DOMContentLoaded', function () { init(document); }); |
| };
| |
| | |
| NationsGlobe.prototype.updateNationHover = function (ll, clientX, clientY) {
| |
| var hit = this.findNationAtLonLat(ll);
| |
| var page = hit ? hit.page : '';
| |
| var label;
| |
| var url;
| |
| | |
| if (!hit || !page) { | |
| this.clearNationHover();
| |
| return null;
| |
| }
| |
| | |
| this.currentNation = {
| |
| page: page,
| |
| entry: this.getNationEntry(page),
| |
| lon: ll.lon,
| |
| lat: ll.lat
| |
| };
| |
| | |
| label = this.getNationLabel(page);
| |
| url = this.getNationUrl(page);
| |
| | |
| this.root.classList.add('has-nation-hover');
| |
| | |
| if (this.nationTooltip) {
| |
| this.nationTooltip.innerHTML = '<span class="nation-tooltip-name">' + escapeHtml(label) + '</span>' + | |
| '<span class="nation-tooltip-code">' + escapeHtml(page) + '</span>' +
| |
| (url ? '<span class="nation-tooltip-link">클릭하여 문서 열기 ›</span>' : '<span class="nation-tooltip-link">링크 미지정</span>');
| |
| this.nationTooltip.classList.add('is-visible');
| |
| this.nationTooltip.setAttribute('aria-hidden', 'false');
| |
| this.positionNationTooltip(clientX, clientY);
| |
| }
| |
| | |
| return this.currentNation;
| |
| };
| |
| | |
| NationsGlobe.prototype.clearNationHover = function () {
| |
| this.currentNation = null;
| |
| this.root.classList.remove('has-nation-hover');
| |
| | |
| if (this.nationTooltip) {
| |
| this.nationTooltip.classList.remove('is-visible');
| |
| this.nationTooltip.setAttribute('aria-hidden', 'true');
| |
| }
| |
| };
| |
| | |
| NationsGlobe.prototype.handleNationClick = function (e) {
| |
| var dx;
| |
| var dy;
| |
| var moved;
| |
| var page;
| |
| var url;
| |
| | |
| if (!e || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') return false;
| |
| if (this.pointerDownX === null || this.pointerDownY === null) return false;
| |
| | |
| dx = e.clientX - this.pointerDownX;
| |
| dy = e.clientY - this.pointerDownY;
| |
| moved = Math.sqrt(dx * dx + dy * dy);
| |
| page = this.currentNation ? this.currentNation.page : this.pointerDownNationPage;
| |
| | |
| this.pointerDownX = null;
| |
| this.pointerDownY = null;
| |
| this.pointerDownNationPage = null;
| |
| | |
| if (moved > this.clickMoveTolerance || !page) return false;
| |
| | |
| url = this.getNationUrl(page);
| |
| if (!url) return false;
| |
| | |
| if (e.ctrlKey || e.metaKey || e.shiftKey) {
| |
| window.open(url, '_blank', 'noopener');
| |
| } else { | | } else { |
| window.location.href = url; | | init(document); |
| } | | } |
| | | })(); |
| return true;
| |
| };
| |
| | |
| NationsGlobe.prototype.loadBoundaryData = function () {
| |
| var self = this;
| |
| var THREE = this.THREE;
| |
| var url = this.root.getAttribute('data-border-geojson-url') || this.root.getAttribute('data-topology-url') || this.root.getAttribute('data-geojson-url') || DEFAULT_WORLD_URL;
| |
| var objectName = this.root.getAttribute('data-topology-object') || 'countries';
| |
| | |
| this.setStatus('DATA');
| |
| | |
| return fetch(url, { credentials: 'omit' })
| |
| .then(function (res) {
| |
| if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + url);
| |
| return res.json();
| |
| })
| |
| .then(function (payload) {
| |
| var geojson = extractGeojsonFromPayload(payload, self.topojson, objectName);
| |
| var built = buildLineGeometryFromGeojson(THREE, geojson, self.radius + Math.max(0.0008, Math.min(0.006, self.boundarySurfaceOffset)));
| |
| var material = new THREE.LineBasicMaterial({
| |
| color: 0xb8b8b8,
| |
| transparent: true,
| |
| opacity: 0.58,
| |
| depthWrite: false
| |
| });
| |
| var lines = new THREE.LineSegments(built.geometry, material);
| |
| lines.renderOrder = 8;
| |
| | |
| self.segmentCount = Math.floor(built.segmentCount || 0);
| |
| self.group.add(lines);
| |
| | |
| if (self.pointerInside && self.pointerClientX !== null && self.pointerClientY !== null) {
| |
| self.updatePointerReadoutFromClient(self.pointerClientX, self.pointerClientY);
| |
| } else {
| |
| self.setReadout(self.getBaseReadout());
| |
| }
| |
| });
| |
| };
| |
| | |
| NationsGlobe.prototype.animate = function (now) {
| |
| var self = this;
| |
| var dt = Math.min(50, now - this.lastTime);
| |
| this.lastTime = now;
| |
| | |
| if (this.disposed || !document.body.contains(this.root)) {
| |
| this.dispose();
| |
| return;
| |
| }
| |
| | |
| if (this.stepResetMotion(dt)) {
| |
| /* reset interpolation owns rotation/camera for this frame */
| |
| } else if (this.autoRotate && !this.dragging) {
| |
| this.rotationY += dt * this.autoRotateSpeed * this.getAutoRotateFactor(dt);
| |
| this.applyRotation();
| |
| } else if (!this.dragging && (Math.abs(this.velocityX) > 0.000001 || Math.abs(this.velocityY) > 0.000001)) {
| |
| var decay = Math.pow(this.inertiaDamping, dt / 16.6667);
| |
| | |
| this.rotationY += this.velocityY * dt;
| |
| this.rotationX += this.velocityX * dt;
| |
| this.velocityY *= decay;
| |
| this.velocityX *= decay;
| |
| | |
| if (Math.abs(this.velocityY) < 0.000001) this.velocityY = 0;
| |
| if (Math.abs(this.velocityX) < 0.000001) this.velocityX = 0;
| |
| | |
| this.applyRotation();
| |
| }
| |
| | |
| this.updateCelestialBackdrop(dt, now);
| |
| | |
| if (this.clouds) {
| |
| /*
| |
| * Cloud motion is relative to the rotating earth group.
| |
| * The parent group carries the globe's AUTO rotation; this local drift
| |
| * only represents slow atmospheric flow over that rotating planet.
| |
| */
| |
| this.clouds.rotation.y += dt * this.cloudDriftSpeed;
| |
| this.clouds.rotation.x = Math.sin(now * 0.000018) * this.cloudTiltDrift;
| |
| this.updateCloudOpacity();
| |
| }
| |
| | |
| this.resize();
| |
| | |
| if (this.pointerInside && this.pointerClientX !== null && this.pointerClientY !== null) {
| |
| this.updatePointerReadoutFromClient(this.pointerClientX, this.pointerClientY);
| |
| }
| |
| | |
| if (this.renderer && this.scene && this.camera) {
| |
| this.renderer.render(this.scene, this.camera);
| |
| }
| |
| | |
| this.frame = window.requestAnimationFrame(function (t) {
| |
| self.animate(t);
| |
| });
| |
| };
| |
| | |
| NationsGlobe.prototype.dispose = function () {
| |
| this.disposed = true;
| |
| if (this.frame) window.cancelAnimationFrame(this.frame);
| |
| | |
| if (this.renderer) {
| |
| try { this.renderer.dispose(); } catch (err) {}
| |
| }
| |
| };
| |
| | |
| function initNationsGlobes(root) {
| |
| var scope = root && root.querySelectorAll ? root : document;
| |
| var nodes = scope.querySelectorAll('[data-nations-globe], .clbi-nations-globe-window');
| |
| | |
| Array.prototype.forEach.call(nodes, function (node) {
| |
| var instance = node.CLBI_NationsGlobeInstance || null;
| |
| var hasStage = !!node.querySelector('.clbi-nations-globe-stage');
| |
| | |
| if (node.getAttribute('data-nations-globe-ready') === '1') {
| |
| if (instance && hasStage) {
| |
| instance.rehydrateDom();
| |
| return;
| |
| }
| |
| | |
| if (instance && !hasStage) {
| |
| try { instance.dispose(); } catch (err) {}
| |
| }
| |
| | |
| node.removeAttribute('data-nations-globe-ready');
| |
| node.CLBI_NationsGlobeInstance = null;
| |
| }
| |
| | |
| node.setAttribute('data-nations-globe-ready', '1');
| |
| | |
| instance = new NationsGlobe(node);
| |
| node.CLBI_NationsGlobeInstance = instance;
| |
| instances.push(instance);
| |
| instance.init();
| |
| });
| |
| }
| |
| | |
| function scheduleNationsGlobeRefresh() {
| |
| window.setTimeout(function () { initNationsGlobes(document); }, 0);
| |
| window.setTimeout(function () { initNationsGlobes(document); }, 120);
| |
| window.setTimeout(function () { initNationsGlobes(document); }, 360);
| |
| }
| |
| | |
| | |
| window.CLBI_NationsGlobe = {
| |
| init: initNationsGlobes,
| |
| instances: instances,
| |
| defaultWorldUrl: DEFAULT_WORLD_URL,
| |
| reload: function () {
| |
| initNationsGlobes(document);
| |
| }
| |
| };
| |
| | |
| $(function () {
| |
| initNationsGlobes(document);
| |
| });
| |
| | |
| if (mw && mw.hook) {
| |
| mw.hook('wikipage.content').add(function ($content) {
| |
| initNationsGlobes($content && $content[0] ? $content[0] : document);
| |
| scheduleNationsGlobeRefresh();
| |
| });
| |
| }
| |
| | |
| window.addEventListener('pageshow', scheduleNationsGlobeRefresh);
| |
| window.addEventListener('focus', scheduleNationsGlobeRefresh);
| |
| | |
| document.addEventListener('visibilitychange', function () {
| |
| if (!document.hidden) scheduleNationsGlobeRefresh();
| |
| });
| |
| | |
| })(window.mediaWiki || window.mw, window.jQuery); | |