canvas-globe 0.1.0 → 0.1.2
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 +150 -137
- package/README.md +665 -638
- package/codemeta.json +1 -1
- package/dist/canvas-globe.umd.js +4280 -4265
- package/package.json +56 -56
- package/src/csv.js +151 -151
- package/src/element.js +142 -142
- package/src/export.js +19 -19
- package/src/geo-globe.js +2846 -2846
- package/src/geo.js +286 -286
- package/src/index.js +10 -10
- package/src/media.js +160 -160
- package/src/presets.js +21 -21
- package/src/react.js +60 -60
- package/src/recorder.js +70 -70
- package/src/scenes.js +80 -80
- package/src/texture.js +129 -129
- package/src/themes.js +241 -241
- package/src/viewer.js +130 -130
- package/types/element.d.ts +41 -41
- package/types/index.d.ts +674 -674
- package/types/react.d.ts +15 -15
package/src/scenes.js
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scenes are whole compositions, not just palettes: a preset plus the layers,
|
|
3
|
-
* motion and overlays a given job needs. Presets answer "how should it look",
|
|
4
|
-
* scenes answer "what am I making".
|
|
5
|
-
*/
|
|
6
|
-
export const scenes = {
|
|
7
|
-
/** Live signups on a pricing or landing page. */
|
|
8
|
-
signups: {
|
|
9
|
-
preset: "hologram",
|
|
10
|
-
mode: "globe",
|
|
11
|
-
autoRotate: true,
|
|
12
|
-
rotateSpeed: 0.06,
|
|
13
|
-
cluster: true,
|
|
14
|
-
tooltip: true,
|
|
15
|
-
showViewer: true,
|
|
16
|
-
counter: { label: "signups this week", position: "top-left" },
|
|
17
|
-
},
|
|
18
|
-
/** A regional launch announcement, ready for country media. */
|
|
19
|
-
launch: {
|
|
20
|
-
preset: "atlas",
|
|
21
|
-
mode: "map",
|
|
22
|
-
landStyle: "fill",
|
|
23
|
-
graticule: false,
|
|
24
|
-
stars: false,
|
|
25
|
-
autoRotate: false,
|
|
26
|
-
focus: { isolate: true, outlineWidth: 2, dim: 1 },
|
|
27
|
-
},
|
|
28
|
-
/** "Trusted in N countries" with customer logos. */
|
|
29
|
-
logos: {
|
|
30
|
-
preset: "mono",
|
|
31
|
-
mode: "map",
|
|
32
|
-
markerStyle: "auto",
|
|
33
|
-
labels: false,
|
|
34
|
-
autoRotate: false,
|
|
35
|
-
graticule: false,
|
|
36
|
-
cluster: true,
|
|
37
|
-
clusterRadius: 54,
|
|
38
|
-
},
|
|
39
|
-
/** Where the team is, for a careers page. */
|
|
40
|
-
team: {
|
|
41
|
-
preset: "atlas",
|
|
42
|
-
mode: "map",
|
|
43
|
-
labels: "markers",
|
|
44
|
-
autoRotate: false,
|
|
45
|
-
graticule: false,
|
|
46
|
-
tooltip: true,
|
|
47
|
-
},
|
|
48
|
-
/** Campaign or revenue coverage by country. */
|
|
49
|
-
coverage: {
|
|
50
|
-
preset: "mono",
|
|
51
|
-
mode: "map",
|
|
52
|
-
countryColors: null,
|
|
53
|
-
labels: false,
|
|
54
|
-
autoRotate: false,
|
|
55
|
-
legend: { title: "Coverage", position: "bottom-left" },
|
|
56
|
-
},
|
|
57
|
-
/** Scroll-linked year in review. */
|
|
58
|
-
review: {
|
|
59
|
-
preset: "midnight",
|
|
60
|
-
mode: "globe",
|
|
61
|
-
autoRotate: false,
|
|
62
|
-
arcs: [],
|
|
63
|
-
counter: { label: "customers", position: "bottom-left" },
|
|
64
|
-
},
|
|
65
|
-
/** Traffic between regions. */
|
|
66
|
-
routes: {
|
|
67
|
-
preset: "blueprint",
|
|
68
|
-
mode: "globe",
|
|
69
|
-
autoRotate: true,
|
|
70
|
-
arcLift: 0.32,
|
|
71
|
-
orbits: 2,
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/** Option keys a scene owns, so switching scenes cannot leak state. */
|
|
76
|
-
export const sceneKeys = [
|
|
77
|
-
"preset", "mode", "landStyle", "graticule", "stars", "autoRotate", "rotateSpeed", "cluster",
|
|
78
|
-
"clusterRadius", "tooltip", "labels", "legend", "counter", "focus", "countryColors", "orbits",
|
|
79
|
-
"arcLift", "showViewer", "markerStyle",
|
|
80
|
-
];
|
|
1
|
+
/**
|
|
2
|
+
* Scenes are whole compositions, not just palettes: a preset plus the layers,
|
|
3
|
+
* motion and overlays a given job needs. Presets answer "how should it look",
|
|
4
|
+
* scenes answer "what am I making".
|
|
5
|
+
*/
|
|
6
|
+
export const scenes = {
|
|
7
|
+
/** Live signups on a pricing or landing page. */
|
|
8
|
+
signups: {
|
|
9
|
+
preset: "hologram",
|
|
10
|
+
mode: "globe",
|
|
11
|
+
autoRotate: true,
|
|
12
|
+
rotateSpeed: 0.06,
|
|
13
|
+
cluster: true,
|
|
14
|
+
tooltip: true,
|
|
15
|
+
showViewer: true,
|
|
16
|
+
counter: { label: "signups this week", position: "top-left" },
|
|
17
|
+
},
|
|
18
|
+
/** A regional launch announcement, ready for country media. */
|
|
19
|
+
launch: {
|
|
20
|
+
preset: "atlas",
|
|
21
|
+
mode: "map",
|
|
22
|
+
landStyle: "fill",
|
|
23
|
+
graticule: false,
|
|
24
|
+
stars: false,
|
|
25
|
+
autoRotate: false,
|
|
26
|
+
focus: { isolate: true, outlineWidth: 2, dim: 1 },
|
|
27
|
+
},
|
|
28
|
+
/** "Trusted in N countries" with customer logos. */
|
|
29
|
+
logos: {
|
|
30
|
+
preset: "mono",
|
|
31
|
+
mode: "map",
|
|
32
|
+
markerStyle: "auto",
|
|
33
|
+
labels: false,
|
|
34
|
+
autoRotate: false,
|
|
35
|
+
graticule: false,
|
|
36
|
+
cluster: true,
|
|
37
|
+
clusterRadius: 54,
|
|
38
|
+
},
|
|
39
|
+
/** Where the team is, for a careers page. */
|
|
40
|
+
team: {
|
|
41
|
+
preset: "atlas",
|
|
42
|
+
mode: "map",
|
|
43
|
+
labels: "markers",
|
|
44
|
+
autoRotate: false,
|
|
45
|
+
graticule: false,
|
|
46
|
+
tooltip: true,
|
|
47
|
+
},
|
|
48
|
+
/** Campaign or revenue coverage by country. */
|
|
49
|
+
coverage: {
|
|
50
|
+
preset: "mono",
|
|
51
|
+
mode: "map",
|
|
52
|
+
countryColors: null,
|
|
53
|
+
labels: false,
|
|
54
|
+
autoRotate: false,
|
|
55
|
+
legend: { title: "Coverage", position: "bottom-left" },
|
|
56
|
+
},
|
|
57
|
+
/** Scroll-linked year in review. */
|
|
58
|
+
review: {
|
|
59
|
+
preset: "midnight",
|
|
60
|
+
mode: "globe",
|
|
61
|
+
autoRotate: false,
|
|
62
|
+
arcs: [],
|
|
63
|
+
counter: { label: "customers", position: "bottom-left" },
|
|
64
|
+
},
|
|
65
|
+
/** Traffic between regions. */
|
|
66
|
+
routes: {
|
|
67
|
+
preset: "blueprint",
|
|
68
|
+
mode: "globe",
|
|
69
|
+
autoRotate: true,
|
|
70
|
+
arcLift: 0.32,
|
|
71
|
+
orbits: 2,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Option keys a scene owns, so switching scenes cannot leak state. */
|
|
76
|
+
export const sceneKeys = [
|
|
77
|
+
"preset", "mode", "landStyle", "graticule", "stars", "autoRotate", "rotateSpeed", "cluster",
|
|
78
|
+
"clusterRadius", "tooltip", "labels", "legend", "counter", "focus", "countryColors", "orbits",
|
|
79
|
+
"arcLift", "showViewer", "markerStyle",
|
|
80
|
+
];
|
package/src/texture.js
CHANGED
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps an equirectangular image onto the orthographic sphere, pixel by pixel.
|
|
3
|
-
*
|
|
4
|
-
* The per-pixel inverse projection is the whole cost, so it is rendered at a
|
|
5
|
-
* reduced resolution and upscaled: roughly 2 ms for a 430 px globe, which
|
|
6
|
-
* fits comfortably inside a 30 fps budget.
|
|
7
|
-
*/
|
|
8
|
-
import { D2R, R2D, clamp } from "./geo.js";
|
|
9
|
-
|
|
10
|
-
const makeSurface = (w, h) => {
|
|
11
|
-
if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
|
|
12
|
-
if (typeof document === "undefined") return null;
|
|
13
|
-
const c = document.createElement("canvas");
|
|
14
|
-
c.width = w;
|
|
15
|
-
c.height = h;
|
|
16
|
-
return c;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export class SphereTexture {
|
|
20
|
-
/** `source` is a URL, HTMLImageElement, ImageBitmap or canvas. */
|
|
21
|
-
constructor(source, { maxWidth = 2048, onLoad } = {}) {
|
|
22
|
-
this.ready = false;
|
|
23
|
-
this.error = null;
|
|
24
|
-
this._onLoad = onLoad;
|
|
25
|
-
this._maxWidth = maxWidth;
|
|
26
|
-
if (typeof source === "string") this._loadUrl(source);
|
|
27
|
-
else if (source) this._ingest(source);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
_loadUrl(url) {
|
|
31
|
-
if (typeof Image === "undefined") return;
|
|
32
|
-
const img = new Image();
|
|
33
|
-
// Required so the pixels stay readable when the image is cross-origin.
|
|
34
|
-
img.crossOrigin = "anonymous";
|
|
35
|
-
img.onload = () => this._ingest(img);
|
|
36
|
-
img.onerror = () => {
|
|
37
|
-
this.error = new Error(`canvas-globe: could not load texture "${url}"`);
|
|
38
|
-
this._onLoad?.(this);
|
|
39
|
-
};
|
|
40
|
-
img.src = url;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
_ingest(image) {
|
|
44
|
-
const sw = image.naturalWidth || image.width;
|
|
45
|
-
const sh = image.naturalHeight || image.height;
|
|
46
|
-
if (!sw || !sh) return;
|
|
47
|
-
const w = Math.min(this._maxWidth, sw);
|
|
48
|
-
const h = Math.round((w / sw) * sh);
|
|
49
|
-
const surface = makeSurface(w, h);
|
|
50
|
-
if (!surface) return;
|
|
51
|
-
const ctx = surface.getContext("2d", { willReadFrequently: true });
|
|
52
|
-
ctx.drawImage(image, 0, 0, w, h);
|
|
53
|
-
try {
|
|
54
|
-
const data = ctx.getImageData(0, 0, w, h);
|
|
55
|
-
this.pixels = data.data;
|
|
56
|
-
this.tw = w;
|
|
57
|
-
this.th = h;
|
|
58
|
-
this.ready = true;
|
|
59
|
-
} catch {
|
|
60
|
-
this.error = new Error("canvas-globe: texture is cross-origin and could not be read");
|
|
61
|
-
}
|
|
62
|
-
this._onLoad?.(this);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Paints the lit hemisphere into `ctx`, centred at cx,cy with radius r. */
|
|
66
|
-
draw(ctx, cx, cy, r, lon0, lat0, { step = 2, shade = true } = {}) {
|
|
67
|
-
if (!this.ready || r <= 0) return false;
|
|
68
|
-
const size = Math.max(2, Math.ceil((r * 2) / step));
|
|
69
|
-
if (!this._surface || this._size !== size) {
|
|
70
|
-
this._surface = makeSurface(size, size);
|
|
71
|
-
if (!this._surface) return false;
|
|
72
|
-
this._ctx = this._surface.getContext("2d");
|
|
73
|
-
this._image = this._ctx.createImageData(size, size);
|
|
74
|
-
this._size = size;
|
|
75
|
-
}
|
|
76
|
-
const out = this._image.data;
|
|
77
|
-
const { pixels, tw, th } = this;
|
|
78
|
-
const f0 = lat0 * D2R;
|
|
79
|
-
const s0 = Math.sin(f0), c0 = Math.cos(f0);
|
|
80
|
-
const inv = 2 / size;
|
|
81
|
-
|
|
82
|
-
for (let py = 0; py < size; py++) {
|
|
83
|
-
const yu = 1 - (py + 0.5) * inv;
|
|
84
|
-
const yu2 = yu * yu;
|
|
85
|
-
let o = py * size * 4;
|
|
86
|
-
for (let px = 0; px < size; px++, o += 4) {
|
|
87
|
-
const x = (px + 0.5) * inv - 1;
|
|
88
|
-
const q = x * x + yu2;
|
|
89
|
-
if (q > 1) {
|
|
90
|
-
out[o + 3] = 0;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const z = Math.sqrt(1 - q);
|
|
94
|
-
const ey = c0 * z - s0 * yu;
|
|
95
|
-
const ez = s0 * z + c0 * yu;
|
|
96
|
-
const lat = Math.asin(clamp(ez, -1, 1)) * R2D;
|
|
97
|
-
const lon = lon0 + Math.atan2(x, ey) * R2D;
|
|
98
|
-
let u = Math.floor((((lon + 180) % 360) + 360) % 360 * (tw / 360));
|
|
99
|
-
if (u >= tw) u -= tw;
|
|
100
|
-
const v = clamp(Math.floor(((90 - lat) / 180) * th), 0, th - 1);
|
|
101
|
-
const s = (v * tw + u) * 4;
|
|
102
|
-
const k = shade ? 0.55 + 0.45 * z : 1;
|
|
103
|
-
out[o] = pixels[s] * k;
|
|
104
|
-
out[o + 1] = pixels[s + 1] * k;
|
|
105
|
-
out[o + 2] = pixels[s + 2] * k;
|
|
106
|
-
out[o + 3] = 255;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
this._ctx.putImageData(this._image, 0, 0);
|
|
110
|
-
ctx.drawImage(this._surface, cx - r, cy - r, r * 2, r * 2);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Paints the whole texture into a flat-map viewport. */
|
|
115
|
-
drawFlat(ctx, fwd, w, h) {
|
|
116
|
-
if (!this.ready || !this._surface2) {
|
|
117
|
-
if (!this.ready) return false;
|
|
118
|
-
this._surface2 = makeSurface(this.tw, this.th);
|
|
119
|
-
if (!this._surface2) return false;
|
|
120
|
-
const c = this._surface2.getContext("2d");
|
|
121
|
-
const img = c.createImageData(this.tw, this.th);
|
|
122
|
-
img.data.set(this.pixels);
|
|
123
|
-
c.putImageData(img, 0, 0);
|
|
124
|
-
}
|
|
125
|
-
const a = fwd(-180, 90), b = fwd(180, -90);
|
|
126
|
-
ctx.drawImage(this._surface2, a[0], a[1], b[0] - a[0], b[1] - a[1]);
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Maps an equirectangular image onto the orthographic sphere, pixel by pixel.
|
|
3
|
+
*
|
|
4
|
+
* The per-pixel inverse projection is the whole cost, so it is rendered at a
|
|
5
|
+
* reduced resolution and upscaled: roughly 2 ms for a 430 px globe, which
|
|
6
|
+
* fits comfortably inside a 30 fps budget.
|
|
7
|
+
*/
|
|
8
|
+
import { D2R, R2D, clamp } from "./geo.js";
|
|
9
|
+
|
|
10
|
+
const makeSurface = (w, h) => {
|
|
11
|
+
if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
|
|
12
|
+
if (typeof document === "undefined") return null;
|
|
13
|
+
const c = document.createElement("canvas");
|
|
14
|
+
c.width = w;
|
|
15
|
+
c.height = h;
|
|
16
|
+
return c;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class SphereTexture {
|
|
20
|
+
/** `source` is a URL, HTMLImageElement, ImageBitmap or canvas. */
|
|
21
|
+
constructor(source, { maxWidth = 2048, onLoad } = {}) {
|
|
22
|
+
this.ready = false;
|
|
23
|
+
this.error = null;
|
|
24
|
+
this._onLoad = onLoad;
|
|
25
|
+
this._maxWidth = maxWidth;
|
|
26
|
+
if (typeof source === "string") this._loadUrl(source);
|
|
27
|
+
else if (source) this._ingest(source);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_loadUrl(url) {
|
|
31
|
+
if (typeof Image === "undefined") return;
|
|
32
|
+
const img = new Image();
|
|
33
|
+
// Required so the pixels stay readable when the image is cross-origin.
|
|
34
|
+
img.crossOrigin = "anonymous";
|
|
35
|
+
img.onload = () => this._ingest(img);
|
|
36
|
+
img.onerror = () => {
|
|
37
|
+
this.error = new Error(`canvas-globe: could not load texture "${url}"`);
|
|
38
|
+
this._onLoad?.(this);
|
|
39
|
+
};
|
|
40
|
+
img.src = url;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_ingest(image) {
|
|
44
|
+
const sw = image.naturalWidth || image.width;
|
|
45
|
+
const sh = image.naturalHeight || image.height;
|
|
46
|
+
if (!sw || !sh) return;
|
|
47
|
+
const w = Math.min(this._maxWidth, sw);
|
|
48
|
+
const h = Math.round((w / sw) * sh);
|
|
49
|
+
const surface = makeSurface(w, h);
|
|
50
|
+
if (!surface) return;
|
|
51
|
+
const ctx = surface.getContext("2d", { willReadFrequently: true });
|
|
52
|
+
ctx.drawImage(image, 0, 0, w, h);
|
|
53
|
+
try {
|
|
54
|
+
const data = ctx.getImageData(0, 0, w, h);
|
|
55
|
+
this.pixels = data.data;
|
|
56
|
+
this.tw = w;
|
|
57
|
+
this.th = h;
|
|
58
|
+
this.ready = true;
|
|
59
|
+
} catch {
|
|
60
|
+
this.error = new Error("canvas-globe: texture is cross-origin and could not be read");
|
|
61
|
+
}
|
|
62
|
+
this._onLoad?.(this);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Paints the lit hemisphere into `ctx`, centred at cx,cy with radius r. */
|
|
66
|
+
draw(ctx, cx, cy, r, lon0, lat0, { step = 2, shade = true } = {}) {
|
|
67
|
+
if (!this.ready || r <= 0) return false;
|
|
68
|
+
const size = Math.max(2, Math.ceil((r * 2) / step));
|
|
69
|
+
if (!this._surface || this._size !== size) {
|
|
70
|
+
this._surface = makeSurface(size, size);
|
|
71
|
+
if (!this._surface) return false;
|
|
72
|
+
this._ctx = this._surface.getContext("2d");
|
|
73
|
+
this._image = this._ctx.createImageData(size, size);
|
|
74
|
+
this._size = size;
|
|
75
|
+
}
|
|
76
|
+
const out = this._image.data;
|
|
77
|
+
const { pixels, tw, th } = this;
|
|
78
|
+
const f0 = lat0 * D2R;
|
|
79
|
+
const s0 = Math.sin(f0), c0 = Math.cos(f0);
|
|
80
|
+
const inv = 2 / size;
|
|
81
|
+
|
|
82
|
+
for (let py = 0; py < size; py++) {
|
|
83
|
+
const yu = 1 - (py + 0.5) * inv;
|
|
84
|
+
const yu2 = yu * yu;
|
|
85
|
+
let o = py * size * 4;
|
|
86
|
+
for (let px = 0; px < size; px++, o += 4) {
|
|
87
|
+
const x = (px + 0.5) * inv - 1;
|
|
88
|
+
const q = x * x + yu2;
|
|
89
|
+
if (q > 1) {
|
|
90
|
+
out[o + 3] = 0;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const z = Math.sqrt(1 - q);
|
|
94
|
+
const ey = c0 * z - s0 * yu;
|
|
95
|
+
const ez = s0 * z + c0 * yu;
|
|
96
|
+
const lat = Math.asin(clamp(ez, -1, 1)) * R2D;
|
|
97
|
+
const lon = lon0 + Math.atan2(x, ey) * R2D;
|
|
98
|
+
let u = Math.floor((((lon + 180) % 360) + 360) % 360 * (tw / 360));
|
|
99
|
+
if (u >= tw) u -= tw;
|
|
100
|
+
const v = clamp(Math.floor(((90 - lat) / 180) * th), 0, th - 1);
|
|
101
|
+
const s = (v * tw + u) * 4;
|
|
102
|
+
const k = shade ? 0.55 + 0.45 * z : 1;
|
|
103
|
+
out[o] = pixels[s] * k;
|
|
104
|
+
out[o + 1] = pixels[s + 1] * k;
|
|
105
|
+
out[o + 2] = pixels[s + 2] * k;
|
|
106
|
+
out[o + 3] = 255;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this._ctx.putImageData(this._image, 0, 0);
|
|
110
|
+
ctx.drawImage(this._surface, cx - r, cy - r, r * 2, r * 2);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Paints the whole texture into a flat-map viewport. */
|
|
115
|
+
drawFlat(ctx, fwd, w, h) {
|
|
116
|
+
if (!this.ready || !this._surface2) {
|
|
117
|
+
if (!this.ready) return false;
|
|
118
|
+
this._surface2 = makeSurface(this.tw, this.th);
|
|
119
|
+
if (!this._surface2) return false;
|
|
120
|
+
const c = this._surface2.getContext("2d");
|
|
121
|
+
const img = c.createImageData(this.tw, this.th);
|
|
122
|
+
img.data.set(this.pixels);
|
|
123
|
+
c.putImageData(img, 0, 0);
|
|
124
|
+
}
|
|
125
|
+
const a = fwd(-180, 90), b = fwd(180, -90);
|
|
126
|
+
ctx.drawImage(this._surface2, a[0], a[1], b[0] - a[0], b[1] - a[1]);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|