canvas-globe 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +76 -4
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/codemeta.json +1 -1
- package/dist/canvas-globe.umd.js +182 -6
- package/package.json +34 -2
- package/src/charts.js +458 -0
- package/src/controls.js +187 -0
- package/src/data/places.js +5 -0
- package/src/fx/effects/ambient.js +355 -0
- package/src/fx/effects/data.js +438 -0
- package/src/fx/effects/entrances.js +111 -0
- package/src/fx/effects/interaction.js +850 -0
- package/src/fx/effects/scene.js +571 -0
- package/src/fx/effects/transitions.js +209 -0
- package/src/fx/index.js +52 -0
- package/src/fx/pointer.js +94 -0
- package/src/fx/runtime.js +161 -0
- package/src/geo-globe.js +170 -4
- package/src/license.js +11 -1
- package/src/places.js +97 -0
- package/src/recipes.js +188 -0
- package/src/version.js +1 -1
- package/types/charts.d.ts +82 -0
- package/types/controls.d.ts +54 -0
- package/types/fx-interaction.d.ts +16 -0
- package/types/fx.d.ts +313 -0
- package/types/index.d.ts +48 -0
- package/types/places.d.ts +43 -0
- package/types/recipes.d.ts +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ All notable changes to this package are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.0] - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added an opt-in effect system with 54 composable effects, three paint stages,
|
|
14
|
+
deterministic timeline controls, pointer state, path tracing, and land-point
|
|
15
|
+
sampling through `canvas-globe/fx`.
|
|
16
|
+
- Added nine chart layers in `canvas-globe/charts`, six reversible recipes in
|
|
17
|
+
`canvas-globe/recipes`, and four markup-independent DOM controls in
|
|
18
|
+
`canvas-globe/controls`.
|
|
19
|
+
- Added `canvas-globe/places`, an optional offline search module containing
|
|
20
|
+
6,772 populated places from Natural Earth.
|
|
21
|
+
- Added 26 effect-authoring helpers for easing, deterministic particles,
|
|
22
|
+
scratch buffers, spherical geometry, HUD elements, and pointer interaction.
|
|
23
|
+
- Added five effect and interaction demo pages plus export, type, package, and
|
|
24
|
+
documentation coverage for all new public entry points.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Made auto-rotation, arcs, orbits, and marker pulses follow the seekable clock
|
|
29
|
+
during frame rendering.
|
|
30
|
+
- Kept all add-on modules outside the core entry point, leaving the core bundle
|
|
31
|
+
at 125.0 KB gzipped.
|
|
32
|
+
|
|
33
|
+
## [1.1.1] - 2026-09-17
|
|
34
|
+
|
|
35
|
+
- Prevented the production licensing presentation from covering live globes on
|
|
36
|
+
the official CanvasGlobe website and its Cloudflare preview deployments.
|
|
37
|
+
- Kept the licensing presentation unchanged for customer production domains.
|
|
38
|
+
|
|
9
39
|
## [1.1.0] - 2026-09-17
|
|
10
40
|
|
|
11
41
|
- Added official Vue, Angular, Svelte, and Web Component packages that keep
|
package/README.md
CHANGED
|
@@ -56,6 +56,10 @@ key, tile service, or runtime network request.
|
|
|
56
56
|
- **Viewer location:** estimate a region from the browser time zone without a permission prompt
|
|
57
57
|
- **Live pings:** display recent activity without requiring a CanvasGlobe backend
|
|
58
58
|
- **Recording:** export a WebM clip in the browser
|
|
59
|
+
- **Optional effects:** 54 seekable visual and interaction effects in `canvas-globe/fx`
|
|
60
|
+
- **Chart layers:** nine animated geographic chart treatments in `canvas-globe/charts`
|
|
61
|
+
- **Recipes and controls:** complete looks and DOM bindings without adding framework code
|
|
62
|
+
- **Place search:** an optional 6,772-place search table with no runtime API request
|
|
59
63
|
- **Presets:** ten included visual styles
|
|
60
64
|
- **Day and night:** calculate the solar terminator for a given time
|
|
61
65
|
- **Four projections:** orthographic, equirectangular, Mercator, and Natural Earth
|
|
@@ -135,19 +139,19 @@ your existing application rather than installing a second React copy.
|
|
|
135
139
|
For a plain `<script>` installation, use the versioned UMD build:
|
|
136
140
|
|
|
137
141
|
```html
|
|
138
|
-
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.
|
|
142
|
+
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.2.0/dist/canvas-globe.umd.js"></script>
|
|
139
143
|
```
|
|
140
144
|
|
|
141
145
|
The same npm release is also available from UNPKG:
|
|
142
146
|
|
|
143
147
|
```html
|
|
144
|
-
<script src="https://unpkg.com/canvas-globe@1.
|
|
148
|
+
<script src="https://unpkg.com/canvas-globe@1.2.0/dist/canvas-globe.umd.js"></script>
|
|
145
149
|
```
|
|
146
150
|
|
|
147
151
|
Modern browsers can import the package through an ESM CDN:
|
|
148
152
|
|
|
149
153
|
```js
|
|
150
|
-
import { createGlobe } from "https://esm.sh/canvas-globe@1.
|
|
154
|
+
import { createGlobe } from "https://esm.sh/canvas-globe@1.2.0";
|
|
151
155
|
```
|
|
152
156
|
|
|
153
157
|
Pin an exact version in production so a future release cannot change a deployed page unexpectedly.
|
|
@@ -167,7 +171,7 @@ import { createGlobe } from "canvas-globe";
|
|
|
167
171
|
Or drop the UMD build on a page with no build step at all:
|
|
168
172
|
|
|
169
173
|
```html
|
|
170
|
-
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.
|
|
174
|
+
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.2.0/dist/canvas-globe.umd.js"></script>
|
|
171
175
|
<canvas id="globe" style="width:520px;aspect-ratio:1"></canvas>
|
|
172
176
|
<script>
|
|
173
177
|
CanvasGlobe.createGlobe(document.getElementById("globe"), {
|
|
@@ -284,6 +288,74 @@ npx skills add https://github.com/Shree-hari/canvas-globe --skill canvas-globe
|
|
|
284
288
|
|
|
285
289
|
Or copy the maintained prompt from the [AI-assisted setup guide](https://canvasglobe.swiftools.com/getting-started/ai-assisted-setup). The skill and prompt select the correct framework entry point, include cleanup and accessibility, and require the user to purchase a production license before shipping.
|
|
286
290
|
|
|
291
|
+
## Effects, charts, recipes, controls, and place search
|
|
292
|
+
|
|
293
|
+
The animation and data-visualization modules are separate entry points. The
|
|
294
|
+
core globe remains about 125 KB gzipped, and applications only download the
|
|
295
|
+
modules they import.
|
|
296
|
+
|
|
297
|
+
| Import | Approximate gzip size | Includes |
|
|
298
|
+
| --- | ---: | --- |
|
|
299
|
+
| `canvas-globe/fx` | 25.4 KB | 54 visual, transition, data, camera, and interaction effects plus authoring helpers |
|
|
300
|
+
| `canvas-globe/charts` | 4.3 KB | Nine animated chart layers |
|
|
301
|
+
| `canvas-globe/recipes` | 2.0 KB | Six complete, reversible compositions |
|
|
302
|
+
| `canvas-globe/controls` | 1.8 KB | Search, timeline, threshold, and crossfilter bindings |
|
|
303
|
+
| `canvas-globe/places` | 93.6 KB | Search over 6,772 bundled places |
|
|
304
|
+
|
|
305
|
+
Effects are plain objects installed on a globe. They can be combined, removed,
|
|
306
|
+
and rendered at an exact timeline position:
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
import { createGlobe } from "canvas-globe";
|
|
310
|
+
import { aurora, counterRoll, routeDashes } from "canvas-globe/fx";
|
|
311
|
+
import { tilegram } from "canvas-globe/charts";
|
|
312
|
+
|
|
313
|
+
const globe = createGlobe(canvas, { preset: "midnight", markers });
|
|
314
|
+
globe
|
|
315
|
+
.use(aurora())
|
|
316
|
+
.use(routeDashes({ routes }))
|
|
317
|
+
.use(counterRoll({ to: 21947, caption: "customers", position: "bottom-center" }));
|
|
318
|
+
|
|
319
|
+
globe.renderFrame(1200); // draw the frame at 1.2 seconds
|
|
320
|
+
globe.use(tilegram({ values: countryValues }));
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Use a recipe when you want a complete look rather than individual effects:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
import { applyRecipe, keynoteGlobe } from "canvas-globe/recipes";
|
|
327
|
+
|
|
328
|
+
const undo = applyRecipe(globe, keynoteGlobe({ countries: 68 }));
|
|
329
|
+
undo(); // restores the previous options and removes the recipe effects
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Controls bind elements you already own and return cleanup functions. They do
|
|
333
|
+
not inject markup or styles:
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
import { searchAndFly } from "canvas-globe/controls";
|
|
337
|
+
import { placeSource } from "canvas-globe/places";
|
|
338
|
+
|
|
339
|
+
const unbind = searchAndFly(globe, document.querySelector("#place-search"), {
|
|
340
|
+
source: placeSource(),
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`canvas-globe/places` is optional because its city table is almost as large as
|
|
345
|
+
the country geometry. Core already resolves countries and roughly 300 major
|
|
346
|
+
cities. Import the place module only when broader offline city search is worth
|
|
347
|
+
the extra payload.
|
|
348
|
+
|
|
349
|
+
`seek(ms)` and `renderFrame(ms)` pin the effect clock, auto-rotation, arcs,
|
|
350
|
+
orbits, and marker pulses. `windField` and part of
|
|
351
|
+
`glitch` accumulate the previous frame by design, so reproduce them by
|
|
352
|
+
exporting frames sequentially. Live media, newly fired pings, and a terminator
|
|
353
|
+
using the current time are external dynamic state and should be fixed or
|
|
354
|
+
disabled for frame-exact export.
|
|
355
|
+
|
|
356
|
+
Custom effects use the same public contract and authoring helpers as the
|
|
357
|
+
included effects. See `types/fx.d.ts` for every option and callback signature.
|
|
358
|
+
|
|
287
359
|
## Options
|
|
288
360
|
|
|
289
361
|
| Option | Default | Description |
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -5,6 +5,7 @@ CanvasGlobe includes geographic data derived from these sources:
|
|
|
5
5
|
| Files | Source | Terms |
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| `src/data/world.js` | [Natural Earth 1:110m](https://www.naturalearthdata.com/about/terms-of-use/) through [world-atlas](https://github.com/topojson/world-atlas) | Natural Earth states that its data is public domain |
|
|
8
|
+
| `src/data/places.js` | [Natural Earth 1:10m populated places](https://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-populated-places/) | Natural Earth states that its data is public domain |
|
|
8
9
|
| `src/data/india.js` | [Datameet india-composite](https://github.com/datameet/maps) | CC0 / public-domain dedication as stated by the source repository |
|
|
9
10
|
|
|
10
11
|
The CanvasGlobe Software License Agreement applies to CanvasGlobe's original software
|
package/codemeta.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"identifier": "canvas-globe",
|
|
6
6
|
"description": "CanvasGlobe helps developers add interactive 3D globes and flat world maps to JavaScript and React apps with Canvas 2D, without WebGL, map tiles, API keys, or runtime network calls.",
|
|
7
7
|
"url": "https://canvasglobe.swiftools.com/",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.2.0",
|
|
9
9
|
"codeRepository": "https://github.com/Shree-hari/canvas-globe",
|
|
10
10
|
"issueTracker": "https://github.com/Shree-hari/canvas-globe/issues",
|
|
11
11
|
"downloadUrl": "https://www.npmjs.com/package/canvas-globe",
|
package/dist/canvas-globe.umd.js
CHANGED
|
@@ -1314,7 +1314,7 @@ function drawFitted(ctx, media, box) {
|
|
|
1314
1314
|
}
|
|
1315
1315
|
|
|
1316
1316
|
// Keep in sync with package.json. Release checks enforce this value.
|
|
1317
|
-
const CANVAS_GLOBE_VERSION = "1.
|
|
1317
|
+
const CANVAS_GLOBE_VERSION = "1.2.0";
|
|
1318
1318
|
|
|
1319
1319
|
/** Local license-key checks and production-use presentation helpers. */
|
|
1320
1320
|
|
|
@@ -1342,9 +1342,18 @@ const PRIVATE_HOST_PATTERNS = [
|
|
|
1342
1342
|
/^::1$/,
|
|
1343
1343
|
];
|
|
1344
1344
|
|
|
1345
|
+
const OFFICIAL_SITE_HOSTS = new Set([
|
|
1346
|
+
"canvasglobe.swiftools.com",
|
|
1347
|
+
"canvas-globe-website.pages.dev",
|
|
1348
|
+
]);
|
|
1349
|
+
const OFFICIAL_PREVIEW_SUFFIX = ".canvas-globe-website.pages.dev";
|
|
1350
|
+
|
|
1345
1351
|
const normalizeLicenseKey = (value) =>
|
|
1346
1352
|
typeof value === "string" ? value.trim() : "";
|
|
1347
1353
|
|
|
1354
|
+
const isOfficialSiteHost = (hostname) =>
|
|
1355
|
+
OFFICIAL_SITE_HOSTS.has(hostname) || hostname.endsWith(OFFICIAL_PREVIEW_SUFFIX);
|
|
1356
|
+
|
|
1348
1357
|
/** Returns development, production or unknown for a browser location. */
|
|
1349
1358
|
function inspectRuntime(locationValue = globalThis.location) {
|
|
1350
1359
|
if (!locationValue) return { kind: "unknown", hostname: "", public: false };
|
|
@@ -1391,7 +1400,7 @@ function getLicensePresentation(
|
|
|
1391
1400
|
) {
|
|
1392
1401
|
const status = inspectLicenseKey(value, mode);
|
|
1393
1402
|
const runtime = inspectRuntime(locationValue);
|
|
1394
|
-
if (!mode || status.valid || !runtime.public) {
|
|
1403
|
+
if (!mode || status.valid || !runtime.public || isOfficialSiteHost(runtime.hostname)) {
|
|
1395
1404
|
return { status, runtime, notice: null };
|
|
1396
1405
|
}
|
|
1397
1406
|
return {
|
|
@@ -1411,6 +1420,7 @@ function reportLicenseStatus(value, mode = COMMERCIAL_LICENSE_MODE) {
|
|
|
1411
1420
|
const presentation = getLicensePresentation(value, globalThis.location, mode);
|
|
1412
1421
|
const { status } = presentation;
|
|
1413
1422
|
if (typeof location === "undefined") return status;
|
|
1423
|
+
if (!presentation.notice) return status;
|
|
1414
1424
|
if (status.kind === "missing") {
|
|
1415
1425
|
console.error(
|
|
1416
1426
|
mode
|
|
@@ -1510,10 +1520,24 @@ const DEFAULTS = {
|
|
|
1510
1520
|
onCountryHover: null,
|
|
1511
1521
|
onCountryClick: null,
|
|
1512
1522
|
onRender: null,
|
|
1523
|
+
effects: null,
|
|
1513
1524
|
};
|
|
1514
1525
|
|
|
1515
1526
|
const coord = (v) => (Array.isArray(v) ? [v[0], v[1]] : [v.lon ?? v.lng ?? v.longitude, v.lat ?? v.latitude]);
|
|
1516
1527
|
|
|
1528
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* An effect's own 0→1 progress at a point on the shared clock. Derived purely
|
|
1532
|
+
* from `ms`, so the same millisecond always renders the same frame.
|
|
1533
|
+
*/
|
|
1534
|
+
const phaseOf = (fx, ms) => {
|
|
1535
|
+
const duration = Math.max(1, fx.duration ?? 1000);
|
|
1536
|
+
const total = duration + (fx.hold ?? 0);
|
|
1537
|
+
const elapsed = fx.loop === false ? Math.min(ms, total) : ((ms % total) + total) % total;
|
|
1538
|
+
return Math.min(1, elapsed / duration);
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1517
1541
|
const defaultTooltip = (target, kind) => {
|
|
1518
1542
|
if (kind === "country") return target.name || String(target.id ?? "");
|
|
1519
1543
|
if (kind === "cluster") return `${target.count} in this area`;
|
|
@@ -1568,6 +1592,11 @@ class GeoGlobe {
|
|
|
1568
1592
|
this._licenseHits = [];
|
|
1569
1593
|
this._licenseDismissed = false;
|
|
1570
1594
|
this._licenseHovered = false;
|
|
1595
|
+
this._fx = [];
|
|
1596
|
+
this._clock = null;
|
|
1597
|
+
this._t0 = now();
|
|
1598
|
+
this._spinBase = this.lon;
|
|
1599
|
+
this.pointer = { x: 0, y: 0, lon: 0, lat: 0, over: false };
|
|
1571
1600
|
|
|
1572
1601
|
this._applyWorld();
|
|
1573
1602
|
this._applyMarkers(this.o.markers);
|
|
@@ -1579,12 +1608,138 @@ class GeoGlobe {
|
|
|
1579
1608
|
this.resize();
|
|
1580
1609
|
if (this.o.focus) this.focusOn(this.o.focus, { instant: true });
|
|
1581
1610
|
if (this.o.showViewer) this._initViewer();
|
|
1611
|
+
for (const fx of this.o.effects || []) this.use(fx);
|
|
1582
1612
|
this._loop = this._loop.bind(this);
|
|
1583
1613
|
this._raf = requestAnimationFrame(this._loop);
|
|
1584
1614
|
}
|
|
1585
1615
|
|
|
1586
1616
|
/* ------------------------------ public API ------------------------------ */
|
|
1587
1617
|
|
|
1618
|
+
/* --------------------------------- effects ------------------------------- */
|
|
1619
|
+
|
|
1620
|
+
/** Installs an effect. Returns `this`, so calls chain. */
|
|
1621
|
+
use(effect) {
|
|
1622
|
+
if (!effect || typeof effect.frame !== "function") return this;
|
|
1623
|
+
this._fx.push({ fx: effect, state: effect.setup?.(this) ?? {} });
|
|
1624
|
+
this._fx.sort((a, b) => (a.fx.z ?? 0) - (b.fx.z ?? 0));
|
|
1625
|
+
return this.invalidate();
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** Removes an effect by reference or by name. */
|
|
1629
|
+
remove(effect) {
|
|
1630
|
+
const i = this._fx.findIndex((e) => e.fx === effect || e.fx.name === effect);
|
|
1631
|
+
if (i < 0) return this;
|
|
1632
|
+
const [entry] = this._fx.splice(i, 1);
|
|
1633
|
+
entry.fx.dispose?.(entry.state, this);
|
|
1634
|
+
return this.invalidate();
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
clearEffects() {
|
|
1638
|
+
while (this._fx.length) this.remove(this._fx[0].fx);
|
|
1639
|
+
return this;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
get effects() {
|
|
1643
|
+
return this._fx.map((e) => e.fx);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* Pins the effect clock to `ms`. Rendering becomes reproducible, which is
|
|
1648
|
+
* what frame-exact export and seamless loops need. `play()` releases it.
|
|
1649
|
+
*/
|
|
1650
|
+
seek(ms) {
|
|
1651
|
+
// Capture where auto-rotation started from, so longitude can be derived
|
|
1652
|
+
// from the clock instead of accumulated per frame. Only on the way in:
|
|
1653
|
+
// seeking again while already seeked must not shift the origin.
|
|
1654
|
+
if (this._clock == null) this._spinBase = wrapLon(this.lon - this._spinRate() * this._fxTime());
|
|
1655
|
+
this._clock = Math.max(0, ms);
|
|
1656
|
+
if (this._autoSpin()) this.lon = wrapLon(this._spinBase + this._spinRate() * this._clock);
|
|
1657
|
+
return this.invalidate();
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
play() {
|
|
1661
|
+
// Resume the timeline where it was paused rather than snapping back to 0.
|
|
1662
|
+
if (this._clock != null) this._t0 = now() - this._clock;
|
|
1663
|
+
this._clock = null;
|
|
1664
|
+
return this.invalidate();
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/** Draws the single frame that belongs at `ms` on the effect clock. */
|
|
1668
|
+
renderFrame(ms) {
|
|
1669
|
+
return this.seek(ms).render();
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
_fxTime() {
|
|
1673
|
+
return this._clock != null ? this._clock : now() - this._t0;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Wall clock for looping scene animation. Pinned while seeking, so arcs,
|
|
1678
|
+
* orbits and pulses land on the same phase every time a frame is redrawn.
|
|
1679
|
+
*/
|
|
1680
|
+
_animTime() {
|
|
1681
|
+
return this._clock != null ? this._clock : Date.now();
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/** Degrees of auto-rotation per millisecond. */
|
|
1685
|
+
_spinRate() {
|
|
1686
|
+
return ((this.o.rotateSpeed || 0) * (this.o.fps || 30)) / 1000;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
_runPasses(stage) {
|
|
1690
|
+
if (!this._fx.length) return;
|
|
1691
|
+
const ms = this._fxTime();
|
|
1692
|
+
const { ctx } = this;
|
|
1693
|
+
for (const entry of this._fx) {
|
|
1694
|
+
if ((entry.fx.stage || "above") !== stage) continue;
|
|
1695
|
+
ctx.save();
|
|
1696
|
+
try {
|
|
1697
|
+
entry.fx.frame(ctx, this, phaseOf(entry.fx, ms), entry.state);
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
// One bad effect must not take the render loop down with it.
|
|
1700
|
+
if (!entry.failed) {
|
|
1701
|
+
entry.failed = true;
|
|
1702
|
+
console.error(`canvas-globe: effect "${entry.fx.name || "anonymous"}" failed`, err);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
ctx.restore();
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/** Land sample points as `[lon, lat]` pairs — what the dot matrix draws. */
|
|
1710
|
+
landPoints(spacing) {
|
|
1711
|
+
const prev = this.o.dotSpacing;
|
|
1712
|
+
if (spacing != null) this.o.dotSpacing = spacing;
|
|
1713
|
+
const flat = this._dotPoints();
|
|
1714
|
+
this.o.dotSpacing = prev;
|
|
1715
|
+
const out = [];
|
|
1716
|
+
for (let i = 0; i < flat.length; i += 2) out.push([flat[i], flat[i + 1]]);
|
|
1717
|
+
return out;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/** Traces a country outline into a path using the current projection. */
|
|
1721
|
+
tracePath(shape, ctx = this.ctx, transform) {
|
|
1722
|
+
const geom = shape?.geometry || shape;
|
|
1723
|
+
if (!geom?.coordinates) return this;
|
|
1724
|
+
const polys = geom.type === "Polygon" ? [geom.coordinates] : geom.coordinates;
|
|
1725
|
+
for (const poly of polys) {
|
|
1726
|
+
for (const ring of poly) {
|
|
1727
|
+
let open = false;
|
|
1728
|
+
for (const c of ring) {
|
|
1729
|
+
const src = transform ? transform(c) : c;
|
|
1730
|
+
const p = this.project(src[0], src[1]);
|
|
1731
|
+
if (!p) {
|
|
1732
|
+
open = false;
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
open ? ctx.lineTo(p.x, p.y) : (ctx.moveTo(p.x, p.y), (open = true));
|
|
1736
|
+
}
|
|
1737
|
+
if (open) ctx.closePath();
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return this;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1588
1743
|
setMarkers(markers = []) {
|
|
1589
1744
|
this._applyMarkers(markers);
|
|
1590
1745
|
this._focus = -1;
|
|
@@ -2141,9 +2296,12 @@ class GeoGlobe {
|
|
|
2141
2296
|
const { w, h } = this._size();
|
|
2142
2297
|
ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0);
|
|
2143
2298
|
ctx.clearRect(0, 0, w, h);
|
|
2299
|
+
this._runPasses("beneath");
|
|
2144
2300
|
this.hits = this.o.mode === "map" ? this._paintMap(w, h) : this._paintGlobe(w, h);
|
|
2145
2301
|
this._dirty = false;
|
|
2302
|
+
this._runPasses("above");
|
|
2146
2303
|
this.o.onRender?.(this);
|
|
2304
|
+
this._runPasses("post");
|
|
2147
2305
|
this._paintLicenseNotice(w, h);
|
|
2148
2306
|
return this;
|
|
2149
2307
|
}
|
|
@@ -2161,6 +2319,7 @@ class GeoGlobe {
|
|
|
2161
2319
|
destroy() {
|
|
2162
2320
|
this._destroyed = true;
|
|
2163
2321
|
cancelAnimationFrame(this._raf);
|
|
2322
|
+
this.clearEffects();
|
|
2164
2323
|
this.stopTour();
|
|
2165
2324
|
this.stopStory();
|
|
2166
2325
|
this.stopTimeline();
|
|
@@ -2615,6 +2774,15 @@ class GeoGlobe {
|
|
|
2615
2774
|
this._onMove = (e) => {
|
|
2616
2775
|
const [x, y] = this._local(e);
|
|
2617
2776
|
this._pointer = { x: e.clientX, y: e.clientY };
|
|
2777
|
+
this.pointer.x = x;
|
|
2778
|
+
this.pointer.y = y;
|
|
2779
|
+
this.pointer.over = true;
|
|
2780
|
+
const at = this.unproject(x, y);
|
|
2781
|
+
if (at) {
|
|
2782
|
+
this.pointer.lon = at[0];
|
|
2783
|
+
this.pointer.lat = at[1];
|
|
2784
|
+
}
|
|
2785
|
+
if (this._fx.length) this._dirty = true;
|
|
2618
2786
|
const licenseHovered = this._licenseHitAt(x, y);
|
|
2619
2787
|
if (licenseHovered !== this._licenseHovered) {
|
|
2620
2788
|
this._licenseHovered = licenseHovered;
|
|
@@ -2685,6 +2853,7 @@ class GeoGlobe {
|
|
|
2685
2853
|
this._onLeave = (e) => {
|
|
2686
2854
|
this._onUp(e);
|
|
2687
2855
|
this._pointer = null;
|
|
2856
|
+
this.pointer.over = false;
|
|
2688
2857
|
this._licenseHovered = false;
|
|
2689
2858
|
if (this._hovered || this._hoveredCountry) {
|
|
2690
2859
|
if (this._hovered) this.o.onHover?.(null, null);
|
|
@@ -2865,13 +3034,20 @@ class GeoGlobe {
|
|
|
2865
3034
|
|
|
2866
3035
|
/* --------------------------------- loop --------------------------------- */
|
|
2867
3036
|
|
|
2868
|
-
|
|
3037
|
+
/** Auto-rotation is configured and permitted, regardless of the clock. */
|
|
3038
|
+
_autoSpin() {
|
|
2869
3039
|
return !!this.o.autoRotate && !this._reducedMotion() && !this._drag && !this._vel && this.o.mode === "globe";
|
|
2870
3040
|
}
|
|
2871
3041
|
|
|
3042
|
+
/** Auto-rotation should advance on its own this frame. */
|
|
3043
|
+
_spinning() {
|
|
3044
|
+
return this._autoSpin() && this._clock == null;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2872
3047
|
/** True while something on screen still needs to move. */
|
|
2873
3048
|
_animating() {
|
|
2874
3049
|
if (this._target || this._drag || this._vel) return true;
|
|
3050
|
+
if (this._fx.length && this._clock == null) return true;
|
|
2875
3051
|
if (this._spinning()) return true;
|
|
2876
3052
|
if (this._reducedMotion()) return false;
|
|
2877
3053
|
if (this._hasLive || this._pings.length) return true;
|
|
@@ -3145,7 +3321,7 @@ class GeoGlobe {
|
|
|
3145
3321
|
const list = this._orbitList();
|
|
3146
3322
|
if (!list.length) return;
|
|
3147
3323
|
const { ctx } = this;
|
|
3148
|
-
const spin = this._reducedMotion() ? 0 : (
|
|
3324
|
+
const spin = this._reducedMotion() ? 0 : (this._animTime() / 1000) % 3600;
|
|
3149
3325
|
ctx.lineCap = "round";
|
|
3150
3326
|
for (const orbit of list) {
|
|
3151
3327
|
const inc = (orbit.inclination ?? 30) * D2R;
|
|
@@ -3699,7 +3875,7 @@ class GeoGlobe {
|
|
|
3699
3875
|
const arcs = this.o.arcs;
|
|
3700
3876
|
if (!arcs || !arcs.length) return;
|
|
3701
3877
|
const { ctx } = this;
|
|
3702
|
-
const now =
|
|
3878
|
+
const now = this._animTime();
|
|
3703
3879
|
const still = this._reducedMotion();
|
|
3704
3880
|
ctx.lineCap = "round";
|
|
3705
3881
|
ctx.lineJoin = "round";
|
|
@@ -4428,7 +4604,7 @@ class GeoGlobe {
|
|
|
4428
4604
|
}
|
|
4429
4605
|
|
|
4430
4606
|
if (m.live && !this._reducedMotion()) {
|
|
4431
|
-
const pulse = (
|
|
4607
|
+
const pulse = (this._animTime() % 2200) / 2200;
|
|
4432
4608
|
ctx.globalAlpha = 1 - pulse;
|
|
4433
4609
|
ctx.strokeStyle = t.live;
|
|
4434
4610
|
ctx.lineWidth = 1.4 * scale;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-globe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Build interactive 3D globes and flat world maps in JavaScript, React, Vue, Angular, or Svelte with Canvas 2D and no WebGL.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": {
|
|
@@ -66,6 +66,36 @@
|
|
|
66
66
|
"import": "./src/react.js",
|
|
67
67
|
"default": "./src/react.js"
|
|
68
68
|
},
|
|
69
|
+
"./fx": {
|
|
70
|
+
"types": "./types/fx.d.ts",
|
|
71
|
+
"import": "./src/fx/index.js",
|
|
72
|
+
"default": "./src/fx/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./fx/interaction": {
|
|
75
|
+
"types": "./types/fx-interaction.d.ts",
|
|
76
|
+
"import": "./src/fx/effects/interaction.js",
|
|
77
|
+
"default": "./src/fx/effects/interaction.js"
|
|
78
|
+
},
|
|
79
|
+
"./recipes": {
|
|
80
|
+
"types": "./types/recipes.d.ts",
|
|
81
|
+
"import": "./src/recipes.js",
|
|
82
|
+
"default": "./src/recipes.js"
|
|
83
|
+
},
|
|
84
|
+
"./charts": {
|
|
85
|
+
"types": "./types/charts.d.ts",
|
|
86
|
+
"import": "./src/charts.js",
|
|
87
|
+
"default": "./src/charts.js"
|
|
88
|
+
},
|
|
89
|
+
"./controls": {
|
|
90
|
+
"types": "./types/controls.d.ts",
|
|
91
|
+
"import": "./src/controls.js",
|
|
92
|
+
"default": "./src/controls.js"
|
|
93
|
+
},
|
|
94
|
+
"./places": {
|
|
95
|
+
"types": "./types/places.d.ts",
|
|
96
|
+
"import": "./src/places.js",
|
|
97
|
+
"default": "./src/places.js"
|
|
98
|
+
},
|
|
69
99
|
"./data/world": {
|
|
70
100
|
"types": "./types/data/world.d.ts",
|
|
71
101
|
"import": "./src/data/world.js",
|
|
@@ -95,7 +125,9 @@
|
|
|
95
125
|
"scripts": {
|
|
96
126
|
"start": "node scripts/serve.mjs",
|
|
97
127
|
"build": "node scripts/build.mjs",
|
|
98
|
-
"data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs",
|
|
128
|
+
"data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs && node scripts/generate-places.mjs",
|
|
129
|
+
"places": "node scripts/generate-places.mjs",
|
|
130
|
+
"audit:docs": "node scripts/audit-docs.mjs",
|
|
99
131
|
"example": "node scripts/serve.mjs",
|
|
100
132
|
"storybook": "storybook dev -p 6006",
|
|
101
133
|
"build-storybook": "storybook build",
|