From 46dfc5a2a397402dd686bac4ab03660dadb45257 Mon Sep 17 00:00:00 2001 From: pacMakaveli Date: Fri, 12 Jun 2026 13:29:59 +0100 Subject: [PATCH] style: apply prettier across the project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repo-wide formatting pass to satisfy the standards-template CI step (npx prettier --check .). Cosmetic only — no behavioural changes. The .prettierrc config (singleQuote, semi, printWidth 100, es5 trailing commas) came from the studio51 standards templates; this brings every file into line with it. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/dependabot.yml | 2 +- .github/workflows/ci.yml | 2 +- canvas/canvas.css | 15 +- canvas/engine.js | 12 +- canvas/index.html | 178 ++- docs/ARCHITECTURE.md | 2 +- docs/USAGE.md | 2 +- index.html | 521 +++++-- legacy/data.js | 116 +- legacy/globe.js | 1351 ++++++++++++------ legacy/index.html | 1481 +++++++++++++++----- shared/data.js | 136 +- shared/fps.js | 31 +- shared/util.js | 7 +- svg/engine.js | 87 +- svg/index.html | 321 +++-- svg/layers.js | 621 +++++--- svg/main.js | 74 +- svg/scene.css | 288 +++- 21 files changed, 3636 insertions(+), 1615 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6298254..273c908 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Something isn't working as expected -labels: ["bug"] +labels: ['bug'] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 4589d3f..5b0b648 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ name: Feature request description: Suggest an idea or improvement -labels: ["enhancement"] +labels: ['enhancement'] body: - type: textarea id: problem diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c75e875..01b3ca4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - package-ecosystem: github-actions - directory: "/" + directory: '/' schedule: interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ff4e0c..61073df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,5 +10,5 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: "20" + node-version: '20' - run: npx prettier --check . diff --git a/canvas/canvas.css b/canvas/canvas.css index ba2480e..0955f98 100644 --- a/canvas/canvas.css +++ b/canvas/canvas.css @@ -1,4 +1,13 @@ /* canvas renderer — element-specific styling */ -#globe-canvas{position:absolute;inset:0;width:100%;height:100%;display:block; - cursor:grab;touch-action:none} -#globe-canvas.grabbing{cursor:grabbing} +#globe-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + display: block; + cursor: grab; + touch-action: none; +} +#globe-canvas.grabbing { + cursor: grabbing; +} diff --git a/canvas/engine.js b/canvas/engine.js index d8aa5a6..0034751 100644 --- a/canvas/engine.js +++ b/canvas/engine.js @@ -4,16 +4,18 @@ * there; here we only wire the canvas-specific hooks. Each frame clears the * single canvas and redraws visible layers — immediate mode, no persistent nodes. */ -import { BaseEngine } from "../shared/engine.js"; +import { BaseEngine } from '../shared/engine.js'; export class Engine extends BaseEngine { constructor(opts) { super(opts); this.canvas = opts.canvas; - this.ctx = opts.canvas.getContext("2d", { alpha: true }); + this.ctx = opts.canvas.getContext('2d', { alpha: true }); } - viewportRect() { return this.canvas.getBoundingClientRect(); } + viewportRect() { + return this.canvas.getBoundingClientRect(); + } resizeBackend() { this.canvas.width = Math.round(this.W * this.dpr); @@ -21,7 +23,9 @@ export class Engine extends BaseEngine { this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0); } - dragTarget() { return this.canvas; } + dragTarget() { + return this.canvas; + } renderFrame() { this.ctx.clearRect(0, 0, this.W, this.H); diff --git a/canvas/index.html b/canvas/index.html index bb18695..a1ed53e 100644 --- a/canvas/index.html +++ b/canvas/index.html @@ -1,81 +1,125 @@ - + - - - -games.directory · Activity Globe (Canvas) - - - - - - - -
-
+ + + + games.directory · Activity Globe (Canvas) + + + + + + + +
+
-
- -
+
+ +
-
-
-
g
-
games.directory
+
+
+
g
+
games.directory
+
+
Live activity · worldwide
-
Live activity · worldwide
-
- + - -
loading world map…
+
+
+
+ loading world map… +
+
- - - - + + + + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cd04984..be2e09c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -38,7 +38,7 @@ over a shared core, so the two strategies can be compared head-to-head: - **Canvas** is the recommended one: the highest, most stable frame rate. - **SVG** keeps the original vector-crisp look. -The two renderers share *everything* except the actual painting. Geometry, +The two renderers share _everything_ except the actual painting. Geometry, simulation, the engine, data, config and UI all live in `shared/`, so there's no duplicated logic. Each renderer's `engine.js` (~30–80 lines) and `layers.js` are **rendering only** — all behaviour comes from `shared/`. diff --git a/docs/USAGE.md b/docs/USAGE.md index 732f160..6c6ec49 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -25,7 +25,7 @@ cities, and activity types (label / colour / weight). Then embed the clean hero: ```html ``` diff --git a/index.html b/index.html index b2dcc41..7e00b04 100644 --- a/index.html +++ b/index.html @@ -1,152 +1,387 @@ - + - - - -games.directory · Activity Globe — Canvas vs SVG - - - - - - - -
-
-
g
-
games.directory · Activity Globe
-
-

One globe, two render engines.

-

- A real-time 3D activity globe, built twice over a shared core so the two rendering - strategies can be compared head-to-head. Same scene, same controls, same data — - only the pixels are pushed differently. Each view shows a live FPS meter. -

+ /* ---- side-by-side view ---- */ + body.compare .wrap { + display: none; + } + .stage { + display: none; + flex-direction: column; + height: 100vh; + } + body.compare .stage { + display: flex; + } + .bar { + display: flex; + align-items: center; + gap: 14px; + padding: 10px 16px; + border-bottom: 1px solid var(--line); + background: rgba(5, 6, 12, 0.7); + backdrop-filter: blur(10px); + } + .bar .name { + font-weight: 600; + font-size: 14px; + } + .bar .name b { + color: var(--accent); + } + .bar .spacer { + flex: 1; + } + .frames { + flex: 1; + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 0; + } + .frames .col { + position: relative; + min-width: 0; + border-right: 1px solid var(--line); + } + .frames .col:last-child { + border-right: 0; + } + .frames .col .label { + position: absolute; + top: 10px; + left: 50%; + transform: translateX(-50%); + z-index: 2; + font-family: var(--mono); + font-size: 10.5px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--accent); + background: rgba(5, 6, 12, 0.6); + border: 1px solid var(--line); + border-radius: 8px; + padding: 5px 12px; + pointer-events: none; + } + .frames iframe { + width: 100%; + height: 100%; + border: 0; + display: block; + background: var(--bg); + } + @media (max-width: 760px) { + .cards { + grid-template-columns: 1fr; + } + .frames { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } + .frames .col { + border-right: 0; + border-bottom: 1px solid var(--line); + } + } + + + + +
+
+
g
+
games.directory · Activity Globe
+
+

One globe, two render engines.

+

+ A real-time 3D activity globe, built twice over a shared core so the two rendering + strategies can be compared head-to-head. Same scene, same controls, same data — only the + pixels are pushed differently. Each view shows a live FPS meter. +

- + -
- - Read the write-up -
+
+ + Read the write-up +
-

- Needs a static server (the world map is fetched at runtime): - python3 -m http.server 8000http://localhost:8000
- The links above open demo mode (?demo) with live controls + FPS. The bare - canvas/ or svg/ URL is the clean hero — globe only, no controls.
- Customise HQ / cities / activity types in shared/data.js; scene settings come from the - platform (?config=<url> or window.__GD_SCENE__), bounded by shared/scene-schema.js. -

-
+

+ Needs a static server (the world map is fetched at runtime): + python3 -m http.server 8000http://localhost:8000
+ The links above open demo mode (?demo) with live controls + FPS. The + bare canvas/ or svg/ URL is the clean hero — globe only, no + controls.
+ Customise HQ / cities / activity types in shared/data.js; scene settings come + from the platform (?config=<url> or window.__GD_SCENE__), + bounded by shared/scene-schema.js. +

+
- -
-
- games.directory · side-by-side - - ← Back to chooser -
-
-
Canvas 2D
-
SVG
-
-
+ +
+
+ games.directory · side-by-side + + ← Back to chooser +
+
+
+ Canvas 2D +
+
+ SVG +
+
+
- - + + diff --git a/legacy/data.js b/legacy/data.js index dd80149..3d6b3be 100644 --- a/legacy/data.js +++ b/legacy/data.js @@ -6,69 +6,69 @@ window.GD = window.GD || {}; // games.directory HQ — London, UK -GD.HQ = { name: "games.directory HQ", city: "London", lnglat: [-0.1276, 51.5074] }; +GD.HQ = { name: 'games.directory HQ', city: 'London', lnglat: [-0.1276, 51.5074] }; // Activity types. `color` is the live, user-editable beam colour. GD.ACTIVITY_TYPES = [ - { id: "trophy", label: "Trophy", color: "#FFC93C", enabled: true, weight: 5 }, - { id: "platinum", label: "Platinum trophy", color: "#3DA5FF", enabled: true, weight: 1 }, - { id: "newgame", label: "New game", color: "#FF5252", enabled: true, weight: 3 }, - { id: "friend", label: "New friend", color: "#36D399", enabled: true, weight: 2 }, - { id: "levelup", label: "Level up", color: "#B98CFF", enabled: true, weight: 3 }, - { id: "completed", label: "Game completed", color: "#2DD4D4", enabled: true, weight: 0.7 } + { id: 'trophy', label: 'Trophy', color: '#FFC93C', enabled: true, weight: 5 }, + { id: 'platinum', label: 'Platinum trophy', color: '#3DA5FF', enabled: true, weight: 1 }, + { id: 'newgame', label: 'New game', color: '#FF5252', enabled: true, weight: 3 }, + { id: 'friend', label: 'New friend', color: '#36D399', enabled: true, weight: 2 }, + { id: 'levelup', label: 'Level up', color: '#B98CFF', enabled: true, weight: 3 }, + { id: 'completed', label: 'Game completed', color: '#2DD4D4', enabled: true, weight: 0.7 }, ]; // World cities used as activity origins ([lng, lat]). GD.CITIES = [ - { name: "Tokyo", lnglat: [139.69, 35.68] }, - { name: "Seoul", lnglat: [126.99, 37.55] }, - { name: "Beijing", lnglat: [116.40, 39.90] }, - { name: "Shanghai", lnglat: [121.47, 31.23] }, - { name: "Hong Kong", lnglat: [114.17, 22.32] }, - { name: "Singapore", lnglat: [103.82, 1.35] }, - { name: "Bangkok", lnglat: [100.50, 13.75] }, - { name: "Jakarta", lnglat: [106.85, -6.21] }, - { name: "Manila", lnglat: [120.98, 14.60] }, - { name: "Kuala Lumpur", lnglat: [101.69, 3.14] }, - { name: "Mumbai", lnglat: [72.88, 19.08] }, - { name: "Delhi", lnglat: [77.21, 28.61] }, - { name: "Dubai", lnglat: [55.27, 25.20] }, - { name: "Riyadh", lnglat: [46.71, 24.71] }, - { name: "Tel Aviv", lnglat: [34.78, 32.08] }, - { name: "Istanbul", lnglat: [28.98, 41.01] }, - { name: "Moscow", lnglat: [37.62, 55.75] }, - { name: "Cairo", lnglat: [31.24, 30.04] }, - { name: "Nairobi", lnglat: [36.82, -1.29] }, - { name: "Lagos", lnglat: [3.38, 6.52] }, - { name: "Johannesburg", lnglat: [28.05, -26.20] }, - { name: "Cape Town", lnglat: [18.42, -33.92] }, - { name: "Athens", lnglat: [23.73, 37.98] }, - { name: "Rome", lnglat: [12.50, 41.90] }, - { name: "Madrid", lnglat: [-3.70, 40.42] }, - { name: "Lisbon", lnglat: [-9.14, 38.72] }, - { name: "Paris", lnglat: [2.35, 48.86] }, - { name: "Amsterdam", lnglat: [4.90, 52.37] }, - { name: "Berlin", lnglat: [13.40, 52.52] }, - { name: "Warsaw", lnglat: [21.01, 52.23] }, - { name: "Stockholm", lnglat: [18.07, 59.33] }, - { name: "Reykjavik", lnglat: [-21.94, 64.15] }, - { name: "Dublin", lnglat: [-6.26, 53.35] }, - { name: "New York", lnglat: [-74.01, 40.71] }, - { name: "Boston", lnglat: [-71.06, 42.36] }, - { name: "Miami", lnglat: [-80.19, 25.76] }, - { name: "Chicago", lnglat: [-87.63, 41.88] }, - { name: "Toronto", lnglat: [-79.38, 43.65] }, - { name: "Vancouver", lnglat: [-123.12, 49.28] }, - { name: "Seattle", lnglat: [-122.33, 47.61] }, - { name: "San Francisco", lnglat: [-122.42, 37.77] }, - { name: "Los Angeles", lnglat: [-118.24, 34.05] }, - { name: "Mexico City", lnglat: [-99.13, 19.43] }, - { name: "Bogotá", lnglat: [-74.07, 4.71] }, - { name: "Lima", lnglat: [-77.04, -12.05] }, - { name: "São Paulo", lnglat: [-46.63, -23.55] }, - { name: "Rio de Janeiro",lnglat: [-43.17, -22.91] }, - { name: "Buenos Aires", lnglat: [-58.38, -34.60] }, - { name: "Sydney", lnglat: [151.21, -33.87] }, - { name: "Melbourne", lnglat: [144.96, -37.81] }, - { name: "Auckland", lnglat: [174.76, -36.85] } + { name: 'Tokyo', lnglat: [139.69, 35.68] }, + { name: 'Seoul', lnglat: [126.99, 37.55] }, + { name: 'Beijing', lnglat: [116.4, 39.9] }, + { name: 'Shanghai', lnglat: [121.47, 31.23] }, + { name: 'Hong Kong', lnglat: [114.17, 22.32] }, + { name: 'Singapore', lnglat: [103.82, 1.35] }, + { name: 'Bangkok', lnglat: [100.5, 13.75] }, + { name: 'Jakarta', lnglat: [106.85, -6.21] }, + { name: 'Manila', lnglat: [120.98, 14.6] }, + { name: 'Kuala Lumpur', lnglat: [101.69, 3.14] }, + { name: 'Mumbai', lnglat: [72.88, 19.08] }, + { name: 'Delhi', lnglat: [77.21, 28.61] }, + { name: 'Dubai', lnglat: [55.27, 25.2] }, + { name: 'Riyadh', lnglat: [46.71, 24.71] }, + { name: 'Tel Aviv', lnglat: [34.78, 32.08] }, + { name: 'Istanbul', lnglat: [28.98, 41.01] }, + { name: 'Moscow', lnglat: [37.62, 55.75] }, + { name: 'Cairo', lnglat: [31.24, 30.04] }, + { name: 'Nairobi', lnglat: [36.82, -1.29] }, + { name: 'Lagos', lnglat: [3.38, 6.52] }, + { name: 'Johannesburg', lnglat: [28.05, -26.2] }, + { name: 'Cape Town', lnglat: [18.42, -33.92] }, + { name: 'Athens', lnglat: [23.73, 37.98] }, + { name: 'Rome', lnglat: [12.5, 41.9] }, + { name: 'Madrid', lnglat: [-3.7, 40.42] }, + { name: 'Lisbon', lnglat: [-9.14, 38.72] }, + { name: 'Paris', lnglat: [2.35, 48.86] }, + { name: 'Amsterdam', lnglat: [4.9, 52.37] }, + { name: 'Berlin', lnglat: [13.4, 52.52] }, + { name: 'Warsaw', lnglat: [21.01, 52.23] }, + { name: 'Stockholm', lnglat: [18.07, 59.33] }, + { name: 'Reykjavik', lnglat: [-21.94, 64.15] }, + { name: 'Dublin', lnglat: [-6.26, 53.35] }, + { name: 'New York', lnglat: [-74.01, 40.71] }, + { name: 'Boston', lnglat: [-71.06, 42.36] }, + { name: 'Miami', lnglat: [-80.19, 25.76] }, + { name: 'Chicago', lnglat: [-87.63, 41.88] }, + { name: 'Toronto', lnglat: [-79.38, 43.65] }, + { name: 'Vancouver', lnglat: [-123.12, 49.28] }, + { name: 'Seattle', lnglat: [-122.33, 47.61] }, + { name: 'San Francisco', lnglat: [-122.42, 37.77] }, + { name: 'Los Angeles', lnglat: [-118.24, 34.05] }, + { name: 'Mexico City', lnglat: [-99.13, 19.43] }, + { name: 'Bogotá', lnglat: [-74.07, 4.71] }, + { name: 'Lima', lnglat: [-77.04, -12.05] }, + { name: 'São Paulo', lnglat: [-46.63, -23.55] }, + { name: 'Rio de Janeiro', lnglat: [-43.17, -22.91] }, + { name: 'Buenos Aires', lnglat: [-58.38, -34.6] }, + { name: 'Sydney', lnglat: [151.21, -33.87] }, + { name: 'Melbourne', lnglat: [144.96, -37.81] }, + { name: 'Auckland', lnglat: [174.76, -36.85] }, ]; diff --git a/legacy/globe.js b/legacy/globe.js index 7c760c1..4416388 100644 --- a/legacy/globe.js +++ b/legacy/globe.js @@ -7,18 +7,18 @@ * and GD (data.js). */ (function () { - "use strict"; + 'use strict'; - var SVGNS = "http://www.w3.org/2000/svg"; + var SVGNS = 'http://www.w3.org/2000/svg'; // ---- tunable state (driven by the control panel) ----------------------- var state = { paused: false, - rotSpeed: 6, // degrees / second (auto-rotate) - rate: 3.0, // activities per second - fireworks: true, // celebratory burst on the trigger event - fwTrigger: "completed", // which activity sets off fireworks - types: {} // id -> type object (live colour/enabled/count) + rotSpeed: 6, // degrees / second (auto-rotate) + rate: 3.0, // activities per second + fireworks: true, // celebratory burst on the trigger event + fwTrigger: 'completed', // which activity sets off fireworks + types: {}, // id -> type object (live colour/enabled/count) }; GD.ACTIVITY_TYPES.forEach(function (t) { state.types[t.id] = Object.assign({ count: 0 }, t); @@ -27,94 +27,132 @@ // ---- scene configuration (persisted; driven by the gear panel) --------- var SCENE_DEFAULTS = { // texture - dotSize: 2.9, texture: 0.32, density: "med", landBright: 1, grid: false, + dotSize: 2.9, + texture: 0.32, + density: 'med', + landBright: 1, + grid: false, // atmosphere atmos: 1, // day & night - dayNight: true, darkness: 0.55, cityLights: true, cityBright: 1, + dayNight: true, + darkness: 0.55, + cityLights: true, + cityBright: 1, // aurora - aurora: true, auroraIntensity: 1, auroraLat: 71, auroraSpeed: 1, auroraScheme: "gv", + aurora: true, + auroraIntensity: 1, + auroraLat: 71, + auroraSpeed: 1, + auroraScheme: 'gv', // effects - corona: true, coronaIntensity: 0.1, nodes: true, orbits: true, + corona: true, + coronaIntensity: 0.1, + nodes: true, + orbits: true, // cosmos - shootingStars: true, meteorRate: 0.5, starTwinkle: true, starDrift: true, - atmosPulse: true, beamTrails: true + shootingStars: true, + meteorRate: 0.5, + starTwinkle: true, + starDrift: true, + atmosPulse: true, + beamTrails: true, }; var scene = Object.assign({}, SCENE_DEFAULTS); try { - var saved = JSON.parse(localStorage.getItem("gd-globe-scene") || "{}"); - Object.keys(saved).forEach(function (k) { if (k in scene) scene[k] = saved[k]; }); + var saved = JSON.parse(localStorage.getItem('gd-globe-scene') || '{}'); + Object.keys(saved).forEach(function (k) { + if (k in scene) scene[k] = saved[k]; + }); } catch (e) {} function saveScene() { - try { localStorage.setItem("gd-globe-scene", JSON.stringify(scene)); } catch (e) {} + try { + localStorage.setItem('gd-globe-scene', JSON.stringify(scene)); + } catch (e) {} } var DENSITY_STEP = { sparse: 3.8, med: 3.0, dense: 2.4 }; var AURORA_SCHEMES = { - gv: ["#5cffb0", "#b58cff"], emerald: ["#3dffa0", "#7affd1"], rose: ["#ff7ab0", "#b58cff"] + gv: ['#5cffb0', '#b58cff'], + emerald: ['#3dffa0', '#7affd1'], + rose: ['#ff7ab0', '#b58cff'], }; // ---- geometry / projection --------------------------------------------- - var svg = document.getElementById("globe-svg"); - var sphereEl = document.getElementById("sphere"); - var sphereGlow = document.getElementById("sphere-glow"); - var highlightEl = document.getElementById("sphere-highlight"); - var graticuleEl = document.getElementById("graticule"); - var dotsEl = document.getElementById("land-dots"); - var landS = document.getElementById("land-s"); - var landM = document.getElementById("land-m"); - var landL = document.getElementById("land-l"); - var nightEl = document.getElementById("night"); - var nightCoreEl = document.getElementById("night-core"); - var clipCircle = document.getElementById("clip-circle"); - var city0 = document.getElementById("city-0"); - var city1 = document.getElementById("city-1"); - var city2 = document.getElementById("city-2"); - var citylightsLayer = document.getElementById("citylights"); - var auroraLayer = document.getElementById("aurora"); - var aurNG = document.getElementById("aur-n-g"); - var aurNV = document.getElementById("aur-n-v"); - var aurSG = document.getElementById("aur-s-g"); - var aurSV = document.getElementById("aur-s-v"); - var tickerEl = document.getElementById("ticker"); - var spikesEl = document.getElementById("spikes"); - var nodesLayer = document.getElementById("nodes"); - var orbitsBack = document.getElementById("orbits-back"); - var orbitsFront = document.getElementById("orbits-front"); - var bottomGlow = document.getElementById("bottom-glow"); - var beamsLayer = document.getElementById("beams"); - var impactsLayer = document.getElementById("impacts"); - var fireworksLayer = document.getElementById("fireworks"); - var hqLayer = document.getElementById("hq"); - var shootingLayer = document.getElementById("shooting"); - var svgDefs = svg.querySelector("defs"); - var starsEls = document.querySelectorAll(".stars"); - - var W = 0, H = 0, CX = 0, CY = 0, R = 0; + var svg = document.getElementById('globe-svg'); + var sphereEl = document.getElementById('sphere'); + var sphereGlow = document.getElementById('sphere-glow'); + var highlightEl = document.getElementById('sphere-highlight'); + var graticuleEl = document.getElementById('graticule'); + var dotsEl = document.getElementById('land-dots'); + var landS = document.getElementById('land-s'); + var landM = document.getElementById('land-m'); + var landL = document.getElementById('land-l'); + var nightEl = document.getElementById('night'); + var nightCoreEl = document.getElementById('night-core'); + var clipCircle = document.getElementById('clip-circle'); + var city0 = document.getElementById('city-0'); + var city1 = document.getElementById('city-1'); + var city2 = document.getElementById('city-2'); + var citylightsLayer = document.getElementById('citylights'); + var auroraLayer = document.getElementById('aurora'); + var aurNG = document.getElementById('aur-n-g'); + var aurNV = document.getElementById('aur-n-v'); + var aurSG = document.getElementById('aur-s-g'); + var aurSV = document.getElementById('aur-s-v'); + var tickerEl = document.getElementById('ticker'); + var spikesEl = document.getElementById('spikes'); + var nodesLayer = document.getElementById('nodes'); + var orbitsBack = document.getElementById('orbits-back'); + var orbitsFront = document.getElementById('orbits-front'); + var bottomGlow = document.getElementById('bottom-glow'); + var beamsLayer = document.getElementById('beams'); + var impactsLayer = document.getElementById('impacts'); + var fireworksLayer = document.getElementById('fireworks'); + var hqLayer = document.getElementById('hq'); + var shootingLayer = document.getElementById('shooting'); + var svgDefs = svg.querySelector('defs'); + var starsEls = document.querySelectorAll('.stars'); + + var W = 0, + H = 0, + CX = 0, + CY = 0, + R = 0; var rotation = [-10, -25, 0]; // [lambda, phi, gamma]; start near Europe/Atlantic var projection = d3.geoOrthographic().clipAngle(90).precision(0.4); var pathGen = d3.geoPath(projection); var graticule = d3.geoGraticule().step([20, 20]); - var landFeature = null; // GeoJSON land (for dot containment) - var landDots = []; // [ [lng,lat], ... ] points that fall on land - var beams = []; // active beams + var landFeature = null; // GeoJSON land (for dot containment) + var landDots = []; // [ [lng,lat], ... ] points that fall on land + var beams = []; // active beams function layout() { var rect = svg.getBoundingClientRect(); - W = rect.width; H = rect.height; + W = rect.width; + H = rect.height; CX = W * 0.5; CY = H * 0.5; R = Math.min(W, H) * 0.36; projection.scale(R).translate([CX, CY]).rotate(rotation); - svg.setAttribute("viewBox", "0 0 " + W + " " + H); - - sphereEl.setAttribute("cx", CX); sphereEl.setAttribute("cy", CY); sphereEl.setAttribute("r", R); - sphereGlow.setAttribute("cx", CX); sphereGlow.setAttribute("cy", CY); sphereGlow.setAttribute("r", R * 1.06); - highlightEl.setAttribute("cx", CX - R * 0.32); highlightEl.setAttribute("cy", CY - R * 0.34); - highlightEl.setAttribute("r", R * 1.0); - bottomGlow.setAttribute("cx", CX); bottomGlow.setAttribute("cy", CY + R * 0.86); - bottomGlow.setAttribute("rx", R * 0.62); bottomGlow.setAttribute("ry", R * 0.26); - clipCircle.setAttribute("cx", CX); clipCircle.setAttribute("cy", CY); clipCircle.setAttribute("r", R + 2); + svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H); + + sphereEl.setAttribute('cx', CX); + sphereEl.setAttribute('cy', CY); + sphereEl.setAttribute('r', R); + sphereGlow.setAttribute('cx', CX); + sphereGlow.setAttribute('cy', CY); + sphereGlow.setAttribute('r', R * 1.06); + highlightEl.setAttribute('cx', CX - R * 0.32); + highlightEl.setAttribute('cy', CY - R * 0.34); + highlightEl.setAttribute('r', R * 1.0); + bottomGlow.setAttribute('cx', CX); + bottomGlow.setAttribute('cy', CY + R * 0.86); + bottomGlow.setAttribute('rx', R * 0.62); + bottomGlow.setAttribute('ry', R * 0.26); + clipCircle.setAttribute('cx', CX); + clipCircle.setAttribute('cy', CY); + clipCircle.setAttribute('r', R + 2); } // visibility of a [lng,lat] on the current orthographic hemisphere @@ -129,7 +167,9 @@ // that is the dominant cost. We cache each point's sin/cos(lat) and // lng(rad) at build time and project with a couple of trig ops per frame. var DEG = Math.PI / 180; - var P_lon0 = 0, P_sinLat0 = 0, P_cosLat0 = 1; + var P_lon0 = 0, + P_sinLat0 = 0, + P_cosLat0 = 1; function updateFastProj() { P_lon0 = -rotation[0] * DEG; var lat0 = -rotation[1] * DEG; @@ -140,78 +180,97 @@ // ---- sun position / day-night terminator ------------------------------- // Real subsolar point from the clock; as the globe spins, the terminator // sweeps across the disc and cities light up gold as they enter night. - var sun_lon = 0, sun_sinLat = 0, sun_cosLat = 1; - var sunCircle = d3.geoCircle().radius(90); // night hemisphere (twilight edge) - var coreCircle = d3.geoCircle().radius(116); // deep-night core (excludes twilight band) + var sun_lon = 0, + sun_sinLat = 0, + sun_cosLat = 1; + var sunCircle = d3.geoCircle().radius(90); // night hemisphere (twilight edge) + var coreCircle = d3.geoCircle().radius(116); // deep-night core (excludes twilight band) var lastSun = -1e9; function updateSun(now) { if (now - lastSun < 1000) return; // sun barely moves; recompute ~1/s lastSun = now; var dt = new Date(); var utc = dt.getUTCHours() + dt.getUTCMinutes() / 60 + dt.getUTCSeconds() / 3600; - var lonDeg = -15 * (utc - 12); // subsolar longitude + var lonDeg = -15 * (utc - 12); // subsolar longitude var start = Date.UTC(dt.getUTCFullYear(), 0, 0); var doy = Math.floor((dt - start) / 86400000); var declDeg = -23.44 * Math.cos((360 / 365) * (doy + 10) * DEG); // solar declination sun_lon = lonDeg * DEG; sun_sinLat = Math.sin(declDeg * DEG); sun_cosLat = Math.cos(declDeg * DEG); - sunCircle.center([lonDeg + 180, -declDeg]); // antisolar point = night centre + sunCircle.center([lonDeg + 180, -declDeg]); // antisolar point = night centre coreCircle.center([lonDeg + 180, -declDeg]); } function renderNight() { - if (!scene.dayNight) { nightEl.setAttribute("d", ""); nightCoreEl.setAttribute("d", ""); return; } - nightEl.setAttribute("d", pathGen(sunCircle()) || ""); - nightCoreEl.setAttribute("d", pathGen(coreCircle()) || ""); + if (!scene.dayNight) { + nightEl.setAttribute('d', ''); + nightCoreEl.setAttribute('d', ''); + return; + } + nightEl.setAttribute('d', pathGen(sunCircle()) || ''); + nightCoreEl.setAttribute('d', pathGen(coreCircle()) || ''); } // ---- polar auroras ------------------------------------------------------ // A wobbling band of points near each pole, projected front-only, drawn as // a soft glowing polyline. Breaks the line when it crosses to the back. function auroraBand(baseLat, amp, phase, now) { - var d = "", start = true; + var d = '', + start = true; var t = now * 0.0006 * scene.auroraSpeed; for (var lng = -180; lng <= 180; lng += 4) { var lngR = lng * DEG; - var lat = baseLat - + amp * Math.sin(lngR * 3 + t * 6 + phase) - + amp * 0.5 * Math.sin(lngR * 7 - t * 4 + phase); + var lat = + baseLat + + amp * Math.sin(lngR * 3 + t * 6 + phase) + + amp * 0.5 * Math.sin(lngR * 7 - t * 4 + phase); var latR = lat * DEG; - var sinL = Math.sin(latR), cosL = Math.cos(latR); - var dlon = lngR - P_lon0, cd = Math.cos(dlon); + var sinL = Math.sin(latR), + cosL = Math.cos(latR); + var dlon = lngR - P_lon0, + cd = Math.cos(dlon); var cosc = P_sinLat0 * sinL + P_cosLat0 * cosL * cd; - if (cosc <= 0.02) { start = true; continue; } // back / limb -> break + if (cosc <= 0.02) { + start = true; + continue; + } // back / limb -> break var sd = Math.sin(dlon); var sx = CX + R * (cosL * sd); var sy = CY - R * (P_cosLat0 * sinL - P_sinLat0 * cosL * cd); - d += (start ? "M" : "L") + sx.toFixed(1) + " " + sy.toFixed(1); + d += (start ? 'M' : 'L') + sx.toFixed(1) + ' ' + sy.toFixed(1); start = false; } return d; } function renderAurora(now) { if (!scene.aurora) { - aurNG.setAttribute("d", ""); aurNV.setAttribute("d", ""); - aurSG.setAttribute("d", ""); aurSV.setAttribute("d", ""); + aurNG.setAttribute('d', ''); + aurNV.setAttribute('d', ''); + aurSG.setAttribute('d', ''); + aurSV.setAttribute('d', ''); return; } var bl = scene.auroraLat; - aurNG.setAttribute("d", auroraBand(bl, 4.5, 0, now)); - aurNV.setAttribute("d", auroraBand(bl + 2.5, 5.5, 1.6, now)); - aurSG.setAttribute("d", auroraBand(-bl, 4.5, 2.4, now)); - aurSV.setAttribute("d", auroraBand(-bl - 2.5, 5.5, 3.9, now)); - var t = now * 0.0011, k = scene.auroraIntensity; + aurNG.setAttribute('d', auroraBand(bl, 4.5, 0, now)); + aurNV.setAttribute('d', auroraBand(bl + 2.5, 5.5, 1.6, now)); + aurSG.setAttribute('d', auroraBand(-bl, 4.5, 2.4, now)); + aurSV.setAttribute('d', auroraBand(-bl - 2.5, 5.5, 3.9, now)); + var t = now * 0.0011, + k = scene.auroraIntensity; aurNG.style.opacity = Math.min(1, (0.34 + 0.22 * Math.sin(t)) * k).toFixed(2); - aurNV.style.opacity = Math.min(1, (0.30 + 0.22 * Math.sin(t + 1.7)) * k).toFixed(2); + aurNV.style.opacity = Math.min(1, (0.3 + 0.22 * Math.sin(t + 1.7)) * k).toFixed(2); aurSG.style.opacity = Math.min(1, (0.34 + 0.22 * Math.sin(t + 3.1)) * k).toFixed(2); - aurSV.style.opacity = Math.min(1, (0.30 + 0.22 * Math.sin(t + 4.6)) * k).toFixed(2); + aurSV.style.opacity = Math.min(1, (0.3 + 0.22 * Math.sin(t + 4.6)) * k).toFixed(2); } // ---- live ticker -------------------------------------------------------- var VERBS = { - trophy: "earned a Trophy", platinum: "unlocked a Platinum", - newgame: "started a new game", friend: "made a new friend", - levelup: "leveled up", completed: "completed a game" + trophy: 'earned a Trophy', + platinum: 'unlocked a Platinum', + newgame: 'started a new game', + friend: 'made a new friend', + levelup: 'leveled up', + completed: 'completed a game', }; var lastTick = -1e9; function pushTicker(type, cityName) { @@ -219,33 +278,54 @@ if (now - lastTick < 220) return; // throttle so high rates don't thrash the DOM lastTick = now; var col = state.types[type.id].color; - var line = document.createElement("div"); - line.className = "tk-line"; + var line = document.createElement('div'); + line.className = 'tk-line'; line.innerHTML = - '' + - '' + (VERBS[type.id] || type.label) + - ' \u00b7 ' + cityName + ''; + '' + + '' + + (VERBS[type.id] || type.label) + + ' \u00b7 ' + + cityName + + ''; tickerEl.insertBefore(line, tickerEl.firstChild); while (tickerEl.children.length > 7) tickerEl.removeChild(tickerEl.lastChild); } // ---- build dotted land -------------------------------------------------- - var dotSin, dotCos, dotLng, dotCity, dotGrp, dotTier, dotN = 0; + var dotSin, + dotCos, + dotLng, + dotCity, + dotGrp, + dotTier, + dotN = 0; function buildLandDots() { var step = DENSITY_STEP[scene.density] || 3.0; - var sinA = [], cosA = [], lngA = [], cityA = [], grpA = [], tierA = [], pts = []; + var sinA = [], + cosA = [], + lngA = [], + cityA = [], + grpA = [], + tierA = [], + pts = []; for (var lat = -84; lat <= 84; lat += step) { var ringStep = step / Math.max(0.18, Math.cos(lat * DEG)); for (var lng = -180; lng < 180; lng += ringStep) { if (d3.geoContains(landFeature, [lng, lat])) { var latR = lat * DEG; - sinA.push(Math.sin(latR)); cosA.push(Math.cos(latR)); lngA.push(lng * DEG); + sinA.push(Math.sin(latR)); + cosA.push(Math.cos(latR)); + lngA.push(lng * DEG); pts.push([lng, lat]); cityA.push(Math.random() < 0.45 ? 1 : 0); grpA.push((Math.random() * 3) | 0); // size tier for relief texture: ~46% small, ~37% medium, ~17% large var r = Math.random(); - tierA.push(r < 0.46 ? 0 : (r < 0.83 ? 1 : 2)); + tierA.push(r < 0.46 ? 0 : r < 0.83 ? 1 : 2); } } } @@ -260,8 +340,14 @@ } function renderDots(now) { - var ds = "", dm = "", dl = "", c0 = "", c1 = "", c2 = ""; - var dayN = scene.dayNight, cityOn = scene.cityLights; + var ds = '', + dm = '', + dl = '', + c0 = '', + c1 = '', + c2 = ''; + var dayN = scene.dayNight, + cityOn = scene.cityLights; for (var i = 0; i < dotN; i++) { var dlon = dotLng[i] - P_lon0; var cd = Math.cos(dlon); @@ -270,51 +356,75 @@ var sd = Math.sin(dlon); var sx = (CX + R * (dotCos[i] * sd)) | 0; var sy = (CY - R * (P_cosLat0 * dotSin[i] - P_sinLat0 * dotCos[i] * cd)) | 0; - var seg = "M" + sx + " " + sy + "l.1 0"; + var seg = 'M' + sx + ' ' + sy + 'l.1 0'; var tier = dotTier[i]; - if (tier === 0) ds += seg; else if (tier === 1) dm += seg; else dl += seg; + if (tier === 0) ds += seg; + else if (tier === 1) dm += seg; + else dl += seg; // city lights: only city points currently on the night side if (cityOn && dayN && dotCity[i]) { var sdl = dotLng[i] - sun_lon; var cosSun = sun_sinLat * dotSin[i] + sun_cosLat * dotCos[i] * Math.cos(sdl); if (cosSun < 0.04) { var g = dotGrp[i]; - if (g === 0) c0 += seg; else if (g === 1) c1 += seg; else c2 += seg; + if (g === 0) c0 += seg; + else if (g === 1) c1 += seg; + else c2 += seg; } } } - landS.setAttribute("d", ds); landM.setAttribute("d", dm); landL.setAttribute("d", dl); - city0.setAttribute("d", c0); city1.setAttribute("d", c1); city2.setAttribute("d", c2); + landS.setAttribute('d', ds); + landM.setAttribute('d', dm); + landL.setAttribute('d', dl); + city0.setAttribute('d', c0); + city1.setAttribute('d', c1); + city2.setAttribute('d', c2); // gentle staggered twinkle, scaled by configured brightness - var t = now * 0.0017, cb = scene.cityBright; + var t = now * 0.0017, + cb = scene.cityBright; city0.style.opacity = Math.min(1.4, (0.55 + 0.45 * Math.sin(t)) * cb).toFixed(2); city1.style.opacity = Math.min(1.4, (0.55 + 0.45 * Math.sin(t + 2.1)) * cb).toFixed(2); city2.style.opacity = Math.min(1.4, (0.55 + 0.45 * Math.sin(t + 4.2)) * cb).toFixed(2); } function renderGraticule() { - graticuleEl.setAttribute("d", pathGen(graticule()) || ""); + graticuleEl.setAttribute('d', pathGen(graticule()) || ''); } // ---- radial spike corona ------------------------------------------------ - var spkSin, spkCos, spkLng, spkLenF, spkPhase, spkN = 0; + var spkSin, + spkCos, + spkLng, + spkLenF, + spkPhase, + spkN = 0; function buildSpikes() { - var step = 7; // sparse — the corona is a faint shimmer, not detail - var sinA = [], cosA = [], lngA = [], lenA = [], phA = []; + var step = 7; // sparse — the corona is a faint shimmer, not detail + var sinA = [], + cosA = [], + lngA = [], + lenA = [], + phA = []; for (var lat = -86; lat <= 86; lat += step) { var ringStep = step / Math.max(0.16, Math.cos(lat * DEG)); for (var lng = -180; lng < 180; lng += ringStep) { var latR = lat * DEG; - sinA.push(Math.sin(latR)); cosA.push(Math.cos(latR)); lngA.push(lng * DEG); - lenA.push(0.25 + Math.random() * 0.95); phA.push(Math.random() * Math.PI * 2); + sinA.push(Math.sin(latR)); + cosA.push(Math.cos(latR)); + lngA.push(lng * DEG); + lenA.push(0.25 + Math.random() * 0.95); + phA.push(Math.random() * Math.PI * 2); } } - spkSin = Float64Array.from(sinA); spkCos = Float64Array.from(cosA); - spkLng = Float64Array.from(lngA); spkLenF = Float64Array.from(lenA); - spkPhase = Float64Array.from(phA); spkN = sinA.length; + spkSin = Float64Array.from(sinA); + spkCos = Float64Array.from(cosA); + spkLng = Float64Array.from(lngA); + spkLenF = Float64Array.from(lenA); + spkPhase = Float64Array.from(phA); + spkN = sinA.length; } function renderSpikes(now) { - var d = ""; + var d = ''; var tt = now * 0.0016; for (var i = 0; i < spkN; i++) { var dlon = spkLng[i] - P_lon0; @@ -326,10 +436,11 @@ var py = CY - R * (P_cosLat0 * spkSin[i] - P_sinLat0 * spkCos[i] * cd); var pulse = 0.7 + 0.3 * Math.sin(tt + spkPhase[i]); var len = (0.02 + spkLenF[i] * 0.04) * pulse; - var ex = CX + (px - CX) * (1 + len), ey = CY + (py - CY) * (1 + len); - d += "M" + (px | 0) + " " + (py | 0) + "L" + (ex | 0) + " " + (ey | 0); + var ex = CX + (px - CX) * (1 + len), + ey = CY + (py - CY) * (1 + len); + d += 'M' + (px | 0) + ' ' + (py | 0) + 'L' + (ex | 0) + ' ' + (ey | 0); } - spikesEl.setAttribute("d", d); + spikesEl.setAttribute('d', d); } // ---- bright star nodes -------------------------------------------------- @@ -339,10 +450,16 @@ if (!landDots.length) return; for (var i = 0; i < 18; i++) { var ll = landDots[(Math.random() * landDots.length) | 0]; - var el2 = el("circle", { "class": "node", r: 1 }); - el2.style.filter = "drop-shadow(0 0 4px #cfe6ff)"; + var el2 = el('circle', { class: 'node', r: 1 }); + el2.style.filter = 'drop-shadow(0 0 4px #cfe6ff)'; nodesLayer.appendChild(el2); - nodes.push({ ll: ll, el: el2, phase: Math.random() * Math.PI * 2, sp: 1.5 + Math.random() * 2.5, size: 1.3 + Math.random() * 1.6 }); + nodes.push({ + ll: ll, + el: el2, + phase: Math.random() * Math.PI * 2, + sp: 1.5 + Math.random() * 2.5, + size: 1.3 + Math.random() * 1.6, + }); } } function renderNodes(now) { @@ -350,11 +467,14 @@ for (var i = 0; i < nodes.length; i++) { var n = nodes[i]; var p = projection(n.ll); - if (!p) { n.el.style.opacity = 0; continue; } + if (!p) { + n.el.style.opacity = 0; + continue; + } var tw = 0.35 + 0.65 * Math.abs(Math.sin(tt * n.sp + n.phase)); - n.el.setAttribute("cx", p[0].toFixed(1)); - n.el.setAttribute("cy", p[1].toFixed(1)); - n.el.setAttribute("r", (n.size * (0.7 + 0.3 * tw)).toFixed(2)); + n.el.setAttribute('cx', p[0].toFixed(1)); + n.el.setAttribute('cy', p[1].toFixed(1)); + n.el.setAttribute('r', (n.size * (0.7 + 0.3 * tw)).toFixed(2)); n.el.style.opacity = tw.toFixed(2); } } @@ -365,99 +485,147 @@ orbits = []; var defs = [ { rf: 1.16, incl: 1.15, yaw0: 0.4, spin: 0.05, sat: true }, - { rf: 1.24, incl: 0.6, yaw0: 2.1, spin: -0.035, sat: false }, - { rf: 1.10, incl: 1.45, yaw0: 1.2, spin: 0.06, sat: true }, + { rf: 1.24, incl: 0.6, yaw0: 2.1, spin: -0.035, sat: false }, + { rf: 1.1, incl: 1.45, yaw0: 1.2, spin: 0.06, sat: true }, { rf: 1.32, incl: 0.95, yaw0: 3.0, spin: -0.025, sat: false }, - { rf: 1.19, incl: 0.3, yaw0: 0.8, spin: 0.04, sat: false } + { rf: 1.19, incl: 0.3, yaw0: 0.8, spin: 0.04, sat: false }, ]; defs.forEach(function (cfg) { - var front = el("path", {}); - var back = el("path", {}); + var front = el('path', {}); + var back = el('path', {}); orbitsFront.appendChild(front); orbitsBack.appendChild(back); - var sat = cfg.sat ? el("circle", { "class": "orbit-sat", r: 2 }) : null; - if (sat) { sat.style.filter = "drop-shadow(0 0 5px #bcd9ff)"; orbitsFront.appendChild(sat); } + var sat = cfg.sat ? el('circle', { class: 'orbit-sat', r: 2 }) : null; + if (sat) { + sat.style.filter = 'drop-shadow(0 0 5px #bcd9ff)'; + orbitsFront.appendChild(sat); + } orbits.push({ cfg: cfg, front: front, back: back, sat: sat }); }); } function renderOrbits(now) { var N = 84; for (var o = 0; o < orbits.length; o++) { - var ob = orbits[o], cfg = ob.cfg; + var ob = orbits[o], + cfg = ob.cfg; var Rr = R * cfg.rf; - var ci = Math.cos(cfg.incl), si = Math.sin(cfg.incl); + var ci = Math.cos(cfg.incl), + si = Math.sin(cfg.incl); var yaw = cfg.yaw0 + now * 0.001 * cfg.spin * 6; - var cy_ = Math.cos(yaw), sy = Math.sin(yaw); - var fd = "", bd = "", fStart = true, bStart = true; - var satIdx = (now * 0.0004 * (o + 1)) % 1 * N | 0; + var cy_ = Math.cos(yaw), + sy = Math.sin(yaw); + var fd = '', + bd = '', + fStart = true, + bStart = true; + var satIdx = (((now * 0.0004 * (o + 1)) % 1) * N) | 0; for (var k = 0; k <= N; k++) { var a = (k / N) * Math.PI * 2; - var x0 = Math.cos(a), y0 = Math.sin(a), z0 = 0; + var x0 = Math.cos(a), + y0 = Math.sin(a), + z0 = 0; // tilt around X - var y1 = y0 * ci - z0 * si, z1 = y0 * si + z0 * ci, x1 = x0; + var y1 = y0 * ci - z0 * si, + z1 = y0 * si + z0 * ci, + x1 = x0; // spin around Y - var x2 = x1 * cy_ + z1 * sy, z2 = -x1 * sy + z1 * cy_, y2 = y1; - var sx = CX + x2 * Rr, sySc = CY - y2 * Rr; - if (z2 >= 0) { fd += (fStart ? "M" : "L") + sx.toFixed(1) + " " + sySc.toFixed(1); fStart = false; bStart = true; } - else { bd += (bStart ? "M" : "L") + sx.toFixed(1) + " " + sySc.toFixed(1); bStart = false; fStart = true; } + var x2 = x1 * cy_ + z1 * sy, + z2 = -x1 * sy + z1 * cy_, + y2 = y1; + var sx = CX + x2 * Rr, + sySc = CY - y2 * Rr; + if (z2 >= 0) { + fd += (fStart ? 'M' : 'L') + sx.toFixed(1) + ' ' + sySc.toFixed(1); + fStart = false; + bStart = true; + } else { + bd += (bStart ? 'M' : 'L') + sx.toFixed(1) + ' ' + sySc.toFixed(1); + bStart = false; + fStart = true; + } if (ob.sat && k === satIdx) { - if (z2 >= 0) { ob.sat.setAttribute("cx", sx.toFixed(1)); ob.sat.setAttribute("cy", sySc.toFixed(1)); ob.sat.style.opacity = 1; } - else ob.sat.style.opacity = 0.15; + if (z2 >= 0) { + ob.sat.setAttribute('cx', sx.toFixed(1)); + ob.sat.setAttribute('cy', sySc.toFixed(1)); + ob.sat.style.opacity = 1; + } else ob.sat.style.opacity = 0.15; } } - ob.front.setAttribute("d", fd); - ob.back.setAttribute("d", bd); + ob.front.setAttribute('d', fd); + ob.back.setAttribute('d', bd); } } // ---- HQ marker ---------------------------------------------------------- var hqRing, hqRing2, hqDot, hqLabel; function buildHQ() { - hqRing = el("circle", { class: "hq-ring" }); - hqRing2 = el("circle", { class: "hq-ring hq-ring-2" }); - hqDot = el("circle", { class: "hq-dot", r: 4.5 }); - hqLabel = el("g", { class: "hq-label" }); - var lt = el("text", { class: "hq-label-text", x: 12, y: 4 }); + hqRing = el('circle', { class: 'hq-ring' }); + hqRing2 = el('circle', { class: 'hq-ring hq-ring-2' }); + hqDot = el('circle', { class: 'hq-dot', r: 4.5 }); + hqLabel = el('g', { class: 'hq-label' }); + var lt = el('text', { class: 'hq-label-text', x: 12, y: 4 }); lt.textContent = GD.HQ.name; - var sub = el("text", { class: "hq-label-sub", x: 12, y: 20 }); + var sub = el('text', { class: 'hq-label-sub', x: 12, y: 20 }); sub.textContent = GD.HQ.city; - hqLabel.appendChild(lt); hqLabel.appendChild(sub); - hqLayer.appendChild(hqRing2); hqLayer.appendChild(hqRing); - hqLayer.appendChild(hqDot); hqLayer.appendChild(hqLabel); + hqLabel.appendChild(lt); + hqLabel.appendChild(sub); + hqLayer.appendChild(hqRing2); + hqLayer.appendChild(hqRing); + hqLayer.appendChild(hqDot); + hqLayer.appendChild(hqLabel); } function renderHQ() { var p = projection(GD.HQ.lnglat); var vis = !!p && visible(GD.HQ.lnglat); var op = vis ? 1 : 0; - [hqRing, hqRing2, hqDot, hqLabel].forEach(function (n) { n.style.opacity = op; }); + [hqRing, hqRing2, hqDot, hqLabel].forEach(function (n) { + n.style.opacity = op; + }); if (!vis) return; - hqRing.setAttribute("cx", p[0]); hqRing.setAttribute("cy", p[1]); - hqRing2.setAttribute("cx", p[0]); hqRing2.setAttribute("cy", p[1]); - hqDot.setAttribute("cx", p[0]); hqDot.setAttribute("cy", p[1]); - hqLabel.setAttribute("transform", "translate(" + p[0] + "," + p[1] + ")"); + hqRing.setAttribute('cx', p[0]); + hqRing.setAttribute('cy', p[1]); + hqRing2.setAttribute('cx', p[0]); + hqRing2.setAttribute('cy', p[1]); + hqDot.setAttribute('cx', p[0]); + hqDot.setAttribute('cy', p[1]); + hqLabel.setAttribute('transform', 'translate(' + p[0] + ',' + p[1] + ')'); } // ---- beams -------------------------------------------------------------- - var DRAW_MS = 1500; // time to draw the arc (slower, more graceful) - var HOLD_MS = 700; // glow hold once landed - var FADE_MS = 950; // fade out + var DRAW_MS = 1500; // time to draw the arc (slower, more graceful) + var HOLD_MS = 700; // glow hold once landed + var FADE_MS = 950; // fade out var LIFE_MS = DRAW_MS + HOLD_MS + FADE_MS; function spawnBeam() { if (!visible(GD.HQ.lnglat)) return; // can't land if HQ faces away - var enabled = GD.ACTIVITY_TYPES.filter(function (t) { return state.types[t.id].enabled; }); + var enabled = GD.ACTIVITY_TYPES.filter(function (t) { + return state.types[t.id].enabled; + }); if (!enabled.length) return; // weighted type pick - var total = enabled.reduce(function (s, t) { return s + (state.types[t.id].weight || 1); }, 0); - var r = Math.random() * total, type = enabled[0]; - for (var i = 0; i < enabled.length; i++) { r -= state.types[enabled[i].id].weight || 1; if (r <= 0) { type = enabled[i]; break; } } + var total = enabled.reduce(function (s, t) { + return s + (state.types[t.id].weight || 1); + }, 0); + var r = Math.random() * total, + type = enabled[0]; + for (var i = 0; i < enabled.length; i++) { + r -= state.types[enabled[i].id].weight || 1; + if (r <= 0) { + type = enabled[i]; + break; + } + } // pick a visible source city (try a handful) var city = null; for (var k = 0; k < 8; k++) { var c = GD.CITIES[(Math.random() * GD.CITIES.length) | 0]; - if (visible(c.lnglat)) { city = c; break; } + if (visible(c.lnglat)) { + city = c; + break; + } } if (!city) return; @@ -466,35 +634,51 @@ updateCount(type.id); pushTicker(type, city.name); - var g = el("g", { class: "beam" }); - if (type.id === state.fwTrigger) g.setAttribute("class", "beam beam-special"); - var glow = el("path", { class: "beam-glow" }); - var core = el("path", { class: "beam-core" }); - var hot = el("path", { class: "beam-hot" }); - var spark = el("path", { class: "beam-spark" }); - var head = el("circle", { class: "beam-head", r: 4.2 }); - g.appendChild(glow); g.appendChild(core); g.appendChild(hot); g.appendChild(spark); g.appendChild(head); + var g = el('g', { class: 'beam' }); + if (type.id === state.fwTrigger) g.setAttribute('class', 'beam beam-special'); + var glow = el('path', { class: 'beam-glow' }); + var core = el('path', { class: 'beam-core' }); + var hot = el('path', { class: 'beam-hot' }); + var spark = el('path', { class: 'beam-spark' }); + var head = el('circle', { class: 'beam-head', r: 4.2 }); + g.appendChild(glow); + g.appendChild(core); + g.appendChild(hot); + g.appendChild(spark); + g.appendChild(head); beamsLayer.appendChild(g); var col = state.types[type.id].color; - glow.setAttribute("stroke", col); - core.setAttribute("stroke", col); - spark.setAttribute("stroke", tint(col)); - head.setAttribute("fill", "#fff"); - head.style.filter = "drop-shadow(0 0 7px " + col + ")"; + glow.setAttribute('stroke', col); + core.setAttribute('stroke', col); + spark.setAttribute('stroke', tint(col)); + head.setAttribute('fill', '#fff'); + head.style.filter = 'drop-shadow(0 0 7px ' + col + ')'; beams.push({ - type: type, src: city.lnglat, color: col, - g: g, glow: glow, core: core, hot: hot, spark: spark, head: head, - t0: performance.now(), impacted: false + type: type, + src: city.lnglat, + color: col, + g: g, + glow: glow, + core: core, + hot: hot, + spark: spark, + head: head, + t0: performance.now(), + impacted: false, }); } // a screen-space lifted quadratic arc; returns control point + endpoints - function lerp(a, b, t) { return a + (b - a) * t; } + function lerp(a, b, t) { + return a + (b - a) * t; + } function arcControl(a, b) { - var mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2; - var vx = mx - CX, vy = my - CY; + var mx = (a[0] + b[0]) / 2, + my = (a[1] + b[1]) / 2; + var vx = mx - CX, + vy = my - CY; var vlen = Math.hypot(vx, vy) || 1; var dist = Math.hypot(b[0] - a[0], b[1] - a[1]); var lift = Math.min(R * 0.9, dist * 0.42 + R * 0.12); @@ -507,7 +691,11 @@ for (var i = beams.length - 1; i >= 0; i--) { var b = beams[i]; var age = now - b.t0; - if (age > LIFE_MS) { removeBeam(b); beams.splice(i, 1); continue; } + if (age > LIFE_MS) { + removeBeam(b); + beams.splice(i, 1); + continue; + } var sp = projection(b.src); var srcVis = !!sp && visible(b.src); @@ -521,44 +709,50 @@ var C = arcControl(sp, hqp); var drawP = Math.min(1, age / DRAW_MS); // easeInOutCubic — eases in and settles gently - var t = drawP < 0.5 ? 4 * drawP * drawP * drawP - : 1 - Math.pow(-2 * drawP + 2, 3) / 2; + var t = drawP < 0.5 ? 4 * drawP * drawP * drawP : 1 - Math.pow(-2 * drawP + 2, 3) / 2; // partial arc via De Casteljau subdivision at t — no DOM geometry queries var d, hx, hy; if (t >= 1) { - d = "M" + sp[0] + " " + sp[1] + "Q" + C[0] + " " + C[1] + " " + hqp[0] + " " + hqp[1]; - hx = hqp[0]; hy = hqp[1]; + d = 'M' + sp[0] + ' ' + sp[1] + 'Q' + C[0] + ' ' + C[1] + ' ' + hqp[0] + ' ' + hqp[1]; + hx = hqp[0]; + hy = hqp[1]; } else { - var ax = lerp(sp[0], C[0], t), ay = lerp(sp[1], C[1], t); - var bx = lerp(C[0], hqp[0], t), by = lerp(C[1], hqp[1], t); - hx = lerp(ax, bx, t); hy = lerp(ay, by, t); - d = "M" + sp[0] + " " + sp[1] + "Q" + ax + " " + ay + " " + hx + " " + hy; + var ax = lerp(sp[0], C[0], t), + ay = lerp(sp[1], C[1], t); + var bx = lerp(C[0], hqp[0], t), + by = lerp(C[1], hqp[1], t); + hx = lerp(ax, bx, t); + hy = lerp(ay, by, t); + d = 'M' + sp[0] + ' ' + sp[1] + 'Q' + ax + ' ' + ay + ' ' + hx + ' ' + hy; } - b.glow.setAttribute("d", d); - b.core.setAttribute("d", d); - b.hot.setAttribute("d", d); + b.glow.setAttribute('d', d); + b.core.setAttribute('d', d); + b.hot.setAttribute('d', d); // comet trail: a bright leading sub-arc just behind the head if (scene.beamTrails && t < 1) { - var u0 = Math.max(0, t - 0.17), sd = ""; + var u0 = Math.max(0, t - 0.17), + sd = ''; for (var s = 0; s <= 6; s++) { - var u = u0 + (t - u0) * (s / 6), iu = 1 - u; + var u = u0 + (t - u0) * (s / 6), + iu = 1 - u; var qx = iu * iu * sp[0] + 2 * iu * u * C[0] + u * u * hqp[0]; var qy = iu * iu * sp[1] + 2 * iu * u * C[1] + u * u * hqp[1]; - sd += (s === 0 ? "M" : "L") + qx.toFixed(1) + " " + qy.toFixed(1); + sd += (s === 0 ? 'M' : 'L') + qx.toFixed(1) + ' ' + qy.toFixed(1); } - b.spark.setAttribute("d", sd); + b.spark.setAttribute('d', sd); b.spark.style.opacity = 1; } else { - b.spark.setAttribute("d", ""); + b.spark.setAttribute('d', ''); } // head dot rides the leading edge while drawing if (t < 1) { - b.head.setAttribute("cx", hx.toFixed(1)); b.head.setAttribute("cy", hy.toFixed(1)); + b.head.setAttribute('cx', hx.toFixed(1)); + b.head.setAttribute('cy', hy.toFixed(1)); // gentle size pulse so the comet head shimmers as it travels - b.head.setAttribute("r", (4.2 + Math.sin(age / 90) * 0.7).toFixed(2)); + b.head.setAttribute('r', (4.2 + Math.sin(age / 90) * 0.7).toFixed(2)); b.head.style.opacity = 1; } else { b.head.style.opacity = 0; @@ -581,18 +775,22 @@ } function spawnImpact(p, color) { - var c = el("circle", { class: "impact", cx: p[0], cy: p[1], r: 5, stroke: color }); + var c = el('circle', { class: 'impact', cx: p[0], cy: p[1], r: 5, stroke: color }); impactsLayer.appendChild(c); - var burst = el("circle", { class: "impact-burst", cx: p[0], cy: p[1], r: 3, fill: color }); + var burst = el('circle', { class: 'impact-burst', cx: p[0], cy: p[1], r: 3, fill: color }); impactsLayer.appendChild(burst); var t0 = performance.now(); (function tick() { var a = (performance.now() - t0) / 620; - if (a >= 1) { c.remove(); burst.remove(); return; } + if (a >= 1) { + c.remove(); + burst.remove(); + return; + } var e = 1 - Math.pow(1 - a, 2); - c.setAttribute("r", 5 + e * R * 0.18); + c.setAttribute('r', 5 + e * R * 0.18); c.style.opacity = 1 - a; - burst.setAttribute("r", 3 + e * 6); + burst.setAttribute('r', 3 + e * 6); burst.style.opacity = (1 - a) * 0.9; requestAnimationFrame(tick); })(); @@ -601,35 +799,55 @@ // ---- fireworks ---------------------------------------------------------- var fireParticles = []; function tint(hex) { - var c = (hex || "#ffffff").replace("#", ""); + var c = (hex || '#ffffff').replace('#', ''); if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2]; - var r = parseInt(c.slice(0, 2), 16), g = parseInt(c.slice(2, 4), 16), b = parseInt(c.slice(4, 6), 16); - r = Math.round(r + (255 - r) * 0.5); g = Math.round(g + (255 - g) * 0.5); b = Math.round(b + (255 - b) * 0.5); - return "rgb(" + r + "," + g + "," + b + ")"; + var r = parseInt(c.slice(0, 2), 16), + g = parseInt(c.slice(2, 4), 16), + b = parseInt(c.slice(4, 6), 16); + r = Math.round(r + (255 - r) * 0.5); + g = Math.round(g + (255 - g) * 0.5); + b = Math.round(b + (255 - b) * 0.5); + return 'rgb(' + r + ',' + g + ',' + b + ')'; } function spawnFireworkBarrage(p, color) { spawnBurst(p[0], p[1], color, 1); - setTimeout(function () { spawnBurst(p[0] + (Math.random() * 2 - 1) * R * 0.20, p[1] - R * 0.14 * Math.random() - R * 0.04, color, 0.72); }, 210); - setTimeout(function () { spawnBurst(p[0] + (Math.random() * 2 - 1) * R * 0.24, p[1] - R * 0.02, tint(color), 0.66); }, 410); + setTimeout(function () { + spawnBurst( + p[0] + (Math.random() * 2 - 1) * R * 0.2, + p[1] - R * 0.14 * Math.random() - R * 0.04, + color, + 0.72 + ); + }, 210); + setTimeout(function () { + spawnBurst(p[0] + (Math.random() * 2 - 1) * R * 0.24, p[1] - R * 0.02, tint(color), 0.66); + }, 410); } function spawnBurst(cx, cy, color, scale) { scale = scale || 1; - var flash = el("circle", { "class": "fw-flash", cx: cx, cy: cy, r: 3, fill: "#fff" }); + var flash = el('circle', { class: 'fw-flash', cx: cx, cy: cy, r: 3, fill: '#fff' }); fireworksLayer.appendChild(flash); fireParticles.push({ el: flash, flash: true, t: 0, ttl: 0.34, grow: R * 0.46 * scale }); - var cols = [color, "#ffffff", tint(color)]; + var cols = [color, '#ffffff', tint(color)]; var N = Math.round(48 * scale); for (var i = 0; i < N; i++) { var ang = Math.random() * Math.PI * 2; var core = i < N * 0.28; var speed = (0.42 + Math.random() * 0.95) * R * 1.7 * scale * (core ? 1.3 : 1); - var col = core ? "#fff" : cols[(Math.random() * cols.length) | 0]; - var c = el("circle", { "class": "fw-spark", r: core ? 1.5 : 2.1, fill: col }); - c.style.filter = "drop-shadow(0 0 5px " + col + ")"; + var col = core ? '#fff' : cols[(Math.random() * cols.length) | 0]; + var c = el('circle', { class: 'fw-spark', r: core ? 1.5 : 2.1, fill: col }); + c.style.filter = 'drop-shadow(0 0 5px ' + col + ')'; fireworksLayer.appendChild(c); fireParticles.push({ - el: c, x: cx, y: cy, vx: Math.cos(ang) * speed, vy: Math.sin(ang) * speed, - t: 0, ttl: 0.9 + Math.random() * 0.8, twk: Math.random() * 10, twinkle: Math.random() < 0.55 + el: c, + x: cx, + y: cy, + vx: Math.cos(ang) * speed, + vy: Math.sin(ang) * speed, + t: 0, + ttl: 0.9 + Math.random() * 0.8, + twk: Math.random() * 10, + twinkle: Math.random() < 0.55, }); } } @@ -638,74 +856,132 @@ var grav = R * 1.25; var drag = Math.max(0, 1 - 2.4 * dt); for (var i = fireParticles.length - 1; i >= 0; i--) { - var p = fireParticles[i]; p.t += dt; + var p = fireParticles[i]; + p.t += dt; if (p.flash) { var a = p.t / p.ttl; - if (a >= 1) { p.el.remove(); fireParticles.splice(i, 1); continue; } - p.el.setAttribute("r", (3 + a * p.grow).toFixed(1)); - p.el.style.opacity = (1 - a); + if (a >= 1) { + p.el.remove(); + fireParticles.splice(i, 1); + continue; + } + p.el.setAttribute('r', (3 + a * p.grow).toFixed(1)); + p.el.style.opacity = 1 - a; + continue; + } + if (p.t >= p.ttl) { + p.el.remove(); + fireParticles.splice(i, 1); continue; } - if (p.t >= p.ttl) { p.el.remove(); fireParticles.splice(i, 1); continue; } - p.vx *= drag; p.vy *= drag; p.vy += grav * dt; - p.x += p.vx * dt; p.y += p.vy * dt; + p.vx *= drag; + p.vy *= drag; + p.vy += grav * dt; + p.x += p.vx * dt; + p.y += p.vy * dt; var op = 1 - p.t / p.ttl; if (p.twinkle) op *= 0.45 + 0.55 * Math.abs(Math.sin(p.t * 16 + p.twk)); - p.el.setAttribute("cx", p.x.toFixed(1)); - p.el.setAttribute("cy", p.y.toFixed(1)); + p.el.setAttribute('cx', p.x.toFixed(1)); + p.el.setAttribute('cy', p.y.toFixed(1)); p.el.style.opacity = Math.max(0, op).toFixed(2); } } // ---- shooting stars / meteors ------------------------------------------ - var meteors = [], meteorAcc = 0, meteorSeq = 0; + var meteors = [], + meteorAcc = 0, + meteorSeq = 0; function spawnMeteor() { // start somewhere along the top or right edge, travel down-left var fromRight = Math.random() < 0.45; var x, y; - if (fromRight) { x = W + 40; y = Math.random() * H * 0.6; } - else { x = Math.random() * W * 0.9; y = -40; } + if (fromRight) { + x = W + 40; + y = Math.random() * H * 0.6; + } else { + x = Math.random() * W * 0.9; + y = -40; + } var ang = (Math.random() * 0.5 + 0.62) * Math.PI; // ~112°–203°: down & left - var speed = (W + H) * (0.34 + Math.random() * 0.30); // px/sec - var vx = Math.cos(ang) * speed, vy = Math.abs(Math.sin(ang)) * speed; + var speed = (W + H) * (0.34 + Math.random() * 0.3); // px/sec + var vx = Math.cos(ang) * speed, + vy = Math.abs(Math.sin(ang)) * speed; var len = 90 + Math.random() * 170; - var id = "met-g-" + (meteorSeq++); - var grad = el("linearGradient", { id: id, gradientUnits: "userSpaceOnUse" }); - var s0 = el("stop", { offset: "0%", "stop-color": "#fff", "stop-opacity": "0" }); - var s1 = el("stop", { offset: "55%", "stop-color": "#cfe3ff", "stop-opacity": ".55" }); - var s2 = el("stop", { offset: "100%", "stop-color": "#fff", "stop-opacity": "1" }); - grad.appendChild(s0); grad.appendChild(s1); grad.appendChild(s2); + var id = 'met-g-' + meteorSeq++; + var grad = el('linearGradient', { id: id, gradientUnits: 'userSpaceOnUse' }); + var s0 = el('stop', { offset: '0%', 'stop-color': '#fff', 'stop-opacity': '0' }); + var s1 = el('stop', { offset: '55%', 'stop-color': '#cfe3ff', 'stop-opacity': '.55' }); + var s2 = el('stop', { offset: '100%', 'stop-color': '#fff', 'stop-opacity': '1' }); + grad.appendChild(s0); + grad.appendChild(s1); + grad.appendChild(s2); svgDefs.appendChild(grad); - var g = el("g", {}); - var trail = el("line", { "class": "met-trail", stroke: "url(#" + id + ")", "stroke-width": (1.3 + Math.random() * 1.1).toFixed(1) }); - var head = el("circle", { "class": "met-head", r: (1.4 + Math.random() * 1.2).toFixed(1) }); - head.style.filter = "drop-shadow(0 0 6px #fff) drop-shadow(0 0 12px #9fc6ff)"; - g.appendChild(trail); g.appendChild(head); + var g = el('g', {}); + var trail = el('line', { + class: 'met-trail', + stroke: 'url(#' + id + ')', + 'stroke-width': (1.3 + Math.random() * 1.1).toFixed(1), + }); + var head = el('circle', { class: 'met-head', r: (1.4 + Math.random() * 1.2).toFixed(1) }); + head.style.filter = 'drop-shadow(0 0 6px #fff) drop-shadow(0 0 12px #9fc6ff)'; + g.appendChild(trail); + g.appendChild(head); shootingLayer.appendChild(g); - meteors.push({ g: g, trail: trail, head: head, grad: grad, x: x, y: y, vx: vx, vy: vy, len: len, t: 0, ttl: 0.9 + Math.random() * 0.7 }); + meteors.push({ + g: g, + trail: trail, + head: head, + grad: grad, + x: x, + y: y, + vx: vx, + vy: vy, + len: len, + t: 0, + ttl: 0.9 + Math.random() * 0.7, + }); } function updateMeteors(dt) { if (scene.shootingStars) { meteorAcc += dt * (0.12 + scene.meteorRate * 1.5); var guard = 0; - while (meteorAcc >= 1 && guard < 3) { spawnMeteor(); meteorAcc -= 1; guard++; } - } else { meteorAcc = 0; } + while (meteorAcc >= 1 && guard < 3) { + spawnMeteor(); + meteorAcc -= 1; + guard++; + } + } else { + meteorAcc = 0; + } for (var i = meteors.length - 1; i >= 0; i--) { - var m = meteors[i]; m.t += dt; + var m = meteors[i]; + m.t += dt; if (m.t >= m.ttl || m.x < -120 || m.y > H + 120) { - m.g.remove(); if (m.grad.parentNode) m.grad.remove(); meteors.splice(i, 1); continue; + m.g.remove(); + if (m.grad.parentNode) m.grad.remove(); + meteors.splice(i, 1); + continue; } - m.x += m.vx * dt; m.y += m.vy * dt; + m.x += m.vx * dt; + m.y += m.vy * dt; var sp = Math.hypot(m.vx, m.vy) || 1; - var tx = m.x - (m.vx / sp) * m.len, ty = m.y - (m.vy / sp) * m.len; - m.trail.setAttribute("x1", tx.toFixed(1)); m.trail.setAttribute("y1", ty.toFixed(1)); - m.trail.setAttribute("x2", m.x.toFixed(1)); m.trail.setAttribute("y2", m.y.toFixed(1)); - m.grad.setAttribute("x1", tx.toFixed(1)); m.grad.setAttribute("y1", ty.toFixed(1)); - m.grad.setAttribute("x2", m.x.toFixed(1)); m.grad.setAttribute("y2", m.y.toFixed(1)); - m.head.setAttribute("cx", m.x.toFixed(1)); m.head.setAttribute("cy", m.y.toFixed(1)); + var tx = m.x - (m.vx / sp) * m.len, + ty = m.y - (m.vy / sp) * m.len; + m.trail.setAttribute('x1', tx.toFixed(1)); + m.trail.setAttribute('y1', ty.toFixed(1)); + m.trail.setAttribute('x2', m.x.toFixed(1)); + m.trail.setAttribute('y2', m.y.toFixed(1)); + m.grad.setAttribute('x1', tx.toFixed(1)); + m.grad.setAttribute('y1', ty.toFixed(1)); + m.grad.setAttribute('x2', m.x.toFixed(1)); + m.grad.setAttribute('y2', m.y.toFixed(1)); + m.head.setAttribute('cx', m.x.toFixed(1)); + m.head.setAttribute('cy', m.y.toFixed(1)); // ease in over first 12%, hold, ease out over last 28% - var a = m.t / m.ttl, op = 1; - if (a < 0.12) op = a / 0.12; else if (a > 0.72) op = Math.max(0, 1 - (a - 0.72) / 0.28); + var a = m.t / m.ttl, + op = 1; + if (a < 0.12) op = a / 0.12; + else if (a > 0.72) op = Math.max(0, 1 - (a - 0.72) / 0.28); m.g.style.opacity = op.toFixed(2); } } @@ -728,7 +1004,11 @@ spawnAcc += dt * state.rate; var guard = 0; - while (spawnAcc >= 1 && guard < 12) { spawnBeam(); spawnAcc -= 1; guard++; } + while (spawnAcc >= 1 && guard < 12) { + spawnBeam(); + spawnAcc -= 1; + guard++; + } } else { projection.rotate(rotation); updateFastProj(); @@ -751,19 +1031,25 @@ if (scene.atmosPulse) { var pulse = 0.82 + 0.18 * Math.sin(now * 0.0011); sphereGlow.style.opacity = (0.7 * scene.atmos * pulse).toFixed(3); - bottomGlow.style.opacity = (scene.atmos * (0.88 + 0.12 * Math.sin(now * 0.0011 + 1.2))).toFixed(3); + bottomGlow.style.opacity = ( + scene.atmos * + (0.88 + 0.12 * Math.sin(now * 0.0011 + 1.2)) + ).toFixed(3); } requestAnimationFrame(frame); } // ---- drag to spin ------------------------------------------------------- - var dragging = false, lastX = 0, lastY = 0; + var dragging = false, + lastX = 0, + lastY = 0; function onDown(e) { dragging = true; var pt = pointer(e); - lastX = pt.x; lastY = pt.y; - svg.classList.add("grabbing"); + lastX = pt.x; + lastY = pt.y; + svg.classList.add('grabbing'); e.preventDefault(); } function onMove(e) { @@ -772,9 +1058,13 @@ var k = 0.26; rotation[0] += (pt.x - lastX) * k; rotation[1] = Math.max(-90, Math.min(90, rotation[1] - (pt.y - lastY) * k)); - lastX = pt.x; lastY = pt.y; + lastX = pt.x; + lastY = pt.y; + } + function onUp() { + dragging = false; + svg.classList.remove('grabbing'); } - function onUp() { dragging = false; svg.classList.remove("grabbing"); } function pointer(e) { var t = e.touches ? e.touches[0] : e; return { x: t.clientX, y: t.clientY }; @@ -791,7 +1081,7 @@ function updateCount(id) { var node = document.querySelector('[data-count="' + id + '"]'); if (node) node.textContent = state.types[id].count.toLocaleString(); - var totalNode = document.getElementById("total-count"); + var totalNode = document.getElementById('total-count'); if (totalNode) { var total = 0; for (var k in state.types) total += state.types[k].count; @@ -801,231 +1091,427 @@ // ---- scene settings: apply config to the DOM --------------------------- function applyScene() { - var base = scene.dotSize, tex = scene.texture; + var base = scene.dotSize, + tex = scene.texture; landS.style.strokeWidth = (base * (1 - tex)).toFixed(2); landM.style.strokeWidth = base.toFixed(2); landL.style.strokeWidth = (base * (1 + tex * 1.5)).toFixed(2); dotsEl.style.opacity = scene.landBright; - graticuleEl.style.display = scene.grid ? "" : "none"; + graticuleEl.style.display = scene.grid ? '' : 'none'; sphereGlow.style.opacity = (0.7 * scene.atmos).toFixed(2); bottomGlow.style.opacity = scene.atmos.toFixed(2); var dn = scene.dayNight; - nightEl.style.display = dn ? "" : "none"; - nightCoreEl.style.display = dn ? "" : "none"; + nightEl.style.display = dn ? '' : 'none'; + nightCoreEl.style.display = dn ? '' : 'none'; nightEl.style.opacity = (0.55 * scene.darkness).toFixed(2); nightCoreEl.style.opacity = (0.58 * scene.darkness).toFixed(2); - citylightsLayer.style.display = (dn && scene.cityLights) ? "" : "none"; + citylightsLayer.style.display = dn && scene.cityLights ? '' : 'none'; - auroraLayer.style.display = scene.aurora ? "" : "none"; + auroraLayer.style.display = scene.aurora ? '' : 'none'; var sch = AURORA_SCHEMES[scene.auroraScheme] || AURORA_SCHEMES.gv; - aurNG.style.stroke = sch[0]; aurSG.style.stroke = sch[0]; - aurNV.style.stroke = sch[1]; aurSV.style.stroke = sch[1]; + aurNG.style.stroke = sch[0]; + aurSG.style.stroke = sch[0]; + aurNV.style.stroke = sch[1]; + aurSV.style.stroke = sch[1]; - spikesEl.style.display = scene.corona ? "" : "none"; + spikesEl.style.display = scene.corona ? '' : 'none'; spikesEl.style.opacity = scene.coronaIntensity; - nodesLayer.style.display = scene.nodes ? "" : "none"; - orbitsFront.style.display = scene.orbits ? "" : "none"; - orbitsBack.style.display = scene.orbits ? "" : "none"; + nodesLayer.style.display = scene.nodes ? '' : 'none'; + orbitsFront.style.display = scene.orbits ? '' : 'none'; + orbitsBack.style.display = scene.orbits ? '' : 'none'; // cosmos - shootingLayer.style.display = scene.shootingStars ? "" : "none"; + shootingLayer.style.display = scene.shootingStars ? '' : 'none'; // when the atmosphere pulse is off, the static glow opacity set above stands; // when on, the main loop drives sphereGlow / bottomGlow opacity each frame. - var aDrift1 = scene.starDrift ? "drift1 140s linear infinite" : ""; - var aDrift2 = scene.starDrift ? "drift2 200s linear infinite" : ""; - var aTw1 = scene.starTwinkle ? "tw1 5.5s ease-in-out infinite" : ""; - var aTw2 = scene.starTwinkle ? "tw2 7s ease-in-out infinite" : ""; - if (starsEls[0]) starsEls[0].style.animation = [aDrift1, aTw1].filter(Boolean).join(", "); - if (starsEls[1]) starsEls[1].style.animation = [aDrift2, aTw2].filter(Boolean).join(", "); + var aDrift1 = scene.starDrift ? 'drift1 140s linear infinite' : ''; + var aDrift2 = scene.starDrift ? 'drift2 200s linear infinite' : ''; + var aTw1 = scene.starTwinkle ? 'tw1 5.5s ease-in-out infinite' : ''; + var aTw2 = scene.starTwinkle ? 'tw2 7s ease-in-out infinite' : ''; + if (starsEls[0]) starsEls[0].style.animation = [aDrift1, aTw1].filter(Boolean).join(', '); + if (starsEls[1]) starsEls[1].style.animation = [aDrift2, aTw2].filter(Boolean).join(', '); } // ---- scene settings: build the gear panel ------------------------------ function buildScene() { - var pct = function (v) { return Math.round(v * 100) + "%"; }; + var pct = function (v) { + return Math.round(v * 100) + '%'; + }; var SPEC = [ - { sec: "Texture" }, - { k: "dotSize", t: "slider", l: "Dot size", min: 1.5, max: 4.5, step: 0.1, fmt: function (v) { return v.toFixed(1) + "px"; } }, - { k: "texture", t: "slider", l: "Relief texture", min: 0, max: 0.6, step: 0.02, fmt: function (v) { return Math.round(v / 0.6 * 100) + "%"; } }, - { k: "landBright", t: "slider", l: "Land brightness", min: 0.4, max: 1, step: 0.05, fmt: pct }, - { k: "density", t: "seg", l: "Dot density", opts: [["sparse", "Sparse"], ["med", "Medium"], ["dense", "Dense"]] }, - { k: "grid", t: "toggle", l: "Lat / long grid" }, - { sec: "Atmosphere" }, - { k: "atmos", t: "slider", l: "Atmospheric glow", min: 0, max: 2, step: 0.1, fmt: function (v) { return Math.round(v * 50) + "%"; } }, - { sec: "Day & night" }, - { k: "dayNight", t: "toggle", l: "Day / night shadow" }, - { k: "darkness", t: "slider", l: "Night darkness", min: 0, max: 0.9, step: 0.05, fmt: function (v) { return Math.round(v / 0.9 * 100) + "%"; } }, - { k: "cityLights", t: "toggle", l: "City lights" }, - { k: "cityBright", t: "slider", l: "City brightness", min: 0.3, max: 1.4, step: 0.05, fmt: function (v) { return Math.round(v / 1.4 * 100) + "%"; } }, - { sec: "Aurora" }, - { k: "aurora", t: "toggle", l: "Aurora" }, - { k: "auroraIntensity", t: "slider", l: "Intensity", min: 0, max: 1.5, step: 0.05, fmt: function (v) { return Math.round(v / 1.5 * 100) + "%"; } }, - { k: "auroraLat", t: "slider", l: "Latitude", min: 55, max: 82, step: 1, fmt: function (v) { return v + "\u00b0"; } }, - { k: "auroraSpeed", t: "slider", l: "Speed", min: 0, max: 3, step: 0.1, fmt: function (v) { return v.toFixed(1) + "\u00d7"; } }, - { k: "auroraScheme", t: "seg", l: "Colour", opts: [["gv", "Green\u00b7Violet"], ["emerald", "Emerald"], ["rose", "Rose"]] }, - { sec: "Effects" }, - { k: "corona", t: "toggle", l: "Edge corona" }, - { k: "coronaIntensity", t: "slider", l: "Corona intensity", min: 0, max: 0.4, step: 0.02, fmt: function (v) { return Math.round(v / 0.4 * 100) + "%"; } }, - { k: "nodes", t: "toggle", l: "Star nodes" }, - { k: "orbits", t: "toggle", l: "Orbital rings" }, - { sec: "Cosmos" }, - { k: "shootingStars", t: "toggle", l: "Shooting stars" }, - { k: "meteorRate", t: "slider", l: "Meteor frequency", min: 0, max: 1, step: 0.05, fmt: function (v) { return Math.round(v * 100) + "%"; } }, - { k: "beamTrails", t: "toggle", l: "Comet beam trails" }, - { k: "atmosPulse", t: "toggle", l: "Atmosphere pulse" }, - { k: "starTwinkle", t: "toggle", l: "Star twinkle" }, - { k: "starDrift", t: "toggle", l: "Star drift" } + { sec: 'Texture' }, + { + k: 'dotSize', + t: 'slider', + l: 'Dot size', + min: 1.5, + max: 4.5, + step: 0.1, + fmt: function (v) { + return v.toFixed(1) + 'px'; + }, + }, + { + k: 'texture', + t: 'slider', + l: 'Relief texture', + min: 0, + max: 0.6, + step: 0.02, + fmt: function (v) { + return Math.round((v / 0.6) * 100) + '%'; + }, + }, + { + k: 'landBright', + t: 'slider', + l: 'Land brightness', + min: 0.4, + max: 1, + step: 0.05, + fmt: pct, + }, + { + k: 'density', + t: 'seg', + l: 'Dot density', + opts: [ + ['sparse', 'Sparse'], + ['med', 'Medium'], + ['dense', 'Dense'], + ], + }, + { k: 'grid', t: 'toggle', l: 'Lat / long grid' }, + { sec: 'Atmosphere' }, + { + k: 'atmos', + t: 'slider', + l: 'Atmospheric glow', + min: 0, + max: 2, + step: 0.1, + fmt: function (v) { + return Math.round(v * 50) + '%'; + }, + }, + { sec: 'Day & night' }, + { k: 'dayNight', t: 'toggle', l: 'Day / night shadow' }, + { + k: 'darkness', + t: 'slider', + l: 'Night darkness', + min: 0, + max: 0.9, + step: 0.05, + fmt: function (v) { + return Math.round((v / 0.9) * 100) + '%'; + }, + }, + { k: 'cityLights', t: 'toggle', l: 'City lights' }, + { + k: 'cityBright', + t: 'slider', + l: 'City brightness', + min: 0.3, + max: 1.4, + step: 0.05, + fmt: function (v) { + return Math.round((v / 1.4) * 100) + '%'; + }, + }, + { sec: 'Aurora' }, + { k: 'aurora', t: 'toggle', l: 'Aurora' }, + { + k: 'auroraIntensity', + t: 'slider', + l: 'Intensity', + min: 0, + max: 1.5, + step: 0.05, + fmt: function (v) { + return Math.round((v / 1.5) * 100) + '%'; + }, + }, + { + k: 'auroraLat', + t: 'slider', + l: 'Latitude', + min: 55, + max: 82, + step: 1, + fmt: function (v) { + return v + '\u00b0'; + }, + }, + { + k: 'auroraSpeed', + t: 'slider', + l: 'Speed', + min: 0, + max: 3, + step: 0.1, + fmt: function (v) { + return v.toFixed(1) + '\u00d7'; + }, + }, + { + k: 'auroraScheme', + t: 'seg', + l: 'Colour', + opts: [ + ['gv', 'Green\u00b7Violet'], + ['emerald', 'Emerald'], + ['rose', 'Rose'], + ], + }, + { sec: 'Effects' }, + { k: 'corona', t: 'toggle', l: 'Edge corona' }, + { + k: 'coronaIntensity', + t: 'slider', + l: 'Corona intensity', + min: 0, + max: 0.4, + step: 0.02, + fmt: function (v) { + return Math.round((v / 0.4) * 100) + '%'; + }, + }, + { k: 'nodes', t: 'toggle', l: 'Star nodes' }, + { k: 'orbits', t: 'toggle', l: 'Orbital rings' }, + { sec: 'Cosmos' }, + { k: 'shootingStars', t: 'toggle', l: 'Shooting stars' }, + { + k: 'meteorRate', + t: 'slider', + l: 'Meteor frequency', + min: 0, + max: 1, + step: 0.05, + fmt: function (v) { + return Math.round(v * 100) + '%'; + }, + }, + { k: 'beamTrails', t: 'toggle', l: 'Comet beam trails' }, + { k: 'atmosPulse', t: 'toggle', l: 'Atmosphere pulse' }, + { k: 'starTwinkle', t: 'toggle', l: 'Star twinkle' }, + { k: 'starDrift', t: 'toggle', l: 'Star drift' }, ]; var byKey = {}; - SPEC.forEach(function (c) { if (c.k) byKey[c.k] = c; }); + SPEC.forEach(function (c) { + if (c.k) byKey[c.k] = c; + }); function ctrlHTML(c) { - if (c.sec) return '
' + c.sec + "
"; - if (c.t === "slider") { - return '
' + c.l + - '
' + - '
'; + if (c.sec) return '
' + c.sec + '
'; + if (c.t === 'slider') { + return ( + '
' + + c.l + + '
' + + '
' + ); } - if (c.t === "toggle") { - return '
' + c.l + - '
'; + if (c.t === 'toggle') { + return ( + '
' + + c.l + + '
' + ); } - if (c.t === "seg") { - var b = c.opts.map(function (o) { return '"; }).join(""); - return '
' + c.l + '
' + b + "
"; + if (c.t === 'seg') { + var b = c.opts + .map(function (o) { + return ''; + }) + .join(''); + return ( + '
' + + c.l + + '
' + + b + + '
' + ); } - return ""; + return ''; } - var host = document.getElementById("scene"); - host.innerHTML = SPEC.map(ctrlHTML).join(""); + var host = document.getElementById('scene'); + host.innerHTML = SPEC.map(ctrlHTML).join(''); SPEC.forEach(function (c) { if (!c.k) return; - if (c.t === "slider") { + if (c.t === 'slider') { host.querySelector('[data-k="' + c.k + '"]').value = scene[c.k]; host.querySelector('[data-val="' + c.k + '"]').textContent = c.fmt(+scene[c.k]); - } else if (c.t === "toggle") { - host.querySelector('[data-tog="' + c.k + '"]').setAttribute("aria-pressed", scene[c.k] ? "true" : "false"); - } else if (c.t === "seg") { + } else if (c.t === 'toggle') { + host + .querySelector('[data-tog="' + c.k + '"]') + .setAttribute('aria-pressed', scene[c.k] ? 'true' : 'false'); + } else if (c.t === 'seg') { host.querySelectorAll('[data-seg="' + c.k + '"]').forEach(function (bn) { - bn.classList.toggle("on", bn.getAttribute("data-v") === scene[c.k]); + bn.classList.toggle('on', bn.getAttribute('data-v') === scene[c.k]); }); } }); - host.addEventListener("input", function (e) { - var k = e.target.getAttribute("data-k"); + host.addEventListener('input', function (e) { + var k = e.target.getAttribute('data-k'); if (!k) return; scene[k] = +e.target.value; host.querySelector('[data-val="' + k + '"]').textContent = byKey[k].fmt(scene[k]); - applyScene(); saveScene(); + applyScene(); + saveScene(); }); - host.addEventListener("click", function (e) { - var tog = e.target.closest("[data-tog]"); + host.addEventListener('click', function (e) { + var tog = e.target.closest('[data-tog]'); if (tog) { - var tk = tog.getAttribute("data-tog"); + var tk = tog.getAttribute('data-tog'); scene[tk] = !scene[tk]; - tog.setAttribute("aria-pressed", scene[tk] ? "true" : "false"); - applyScene(); saveScene(); return; + tog.setAttribute('aria-pressed', scene[tk] ? 'true' : 'false'); + applyScene(); + saveScene(); + return; } - var seg = e.target.closest("[data-seg]"); + var seg = e.target.closest('[data-seg]'); if (seg) { - var sk = seg.getAttribute("data-seg"), sv = seg.getAttribute("data-v"); + var sk = seg.getAttribute('data-seg'), + sv = seg.getAttribute('data-v'); scene[sk] = sv; - host.querySelectorAll('[data-seg="' + sk + '"]').forEach(function (bn) { bn.classList.toggle("on", bn === seg); }); - if (sk === "density") { buildLandDots(); renderDots(performance.now()); } - applyScene(); saveScene(); return; + host.querySelectorAll('[data-seg="' + sk + '"]').forEach(function (bn) { + bn.classList.toggle('on', bn === seg); + }); + if (sk === 'density') { + buildLandDots(); + renderDots(performance.now()); + } + applyScene(); + saveScene(); + return; } }); - var gear = document.getElementById("scene-toggle"); + var gear = document.getElementById('scene-toggle'); function setOpen(open) { - if (open) host.removeAttribute("hidden"); else host.setAttribute("hidden", ""); - gear.setAttribute("aria-pressed", open ? "true" : "false"); - try { localStorage.setItem("gd-globe-scene-open", open ? "1" : "0"); } catch (e) {} + if (open) host.removeAttribute('hidden'); + else host.setAttribute('hidden', ''); + gear.setAttribute('aria-pressed', open ? 'true' : 'false'); + try { + localStorage.setItem('gd-globe-scene-open', open ? '1' : '0'); + } catch (e) {} } - gear.addEventListener("click", function () { - setOpen(host.hasAttribute("hidden")); + gear.addEventListener('click', function () { + setOpen(host.hasAttribute('hidden')); }); var wasOpen = false; - try { wasOpen = localStorage.getItem("gd-globe-scene-open") === "1"; } catch (e) {} + try { + wasOpen = localStorage.getItem('gd-globe-scene-open') === '1'; + } catch (e) {} if (wasOpen) setOpen(true); } function buildControls() { - var list = document.getElementById("activity-list"); + var list = document.getElementById('activity-list'); GD.ACTIVITY_TYPES.forEach(function (t) { - var row = document.createElement("div"); - row.className = "act-row"; + var row = document.createElement('div'); + row.className = 'act-row'; row.innerHTML = - '