@motionscript/geo 0.0.0-stage → 0.1.0-alpha.3
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 +21 -0
- package/LICENSE +201 -0
- package/dist/border/format.d.ts +73 -0
- package/dist/border/format.d.ts.map +1 -0
- package/dist/border/format.js +68 -0
- package/dist/border/format.js.map +1 -0
- package/dist/border/geo-border.d.ts +44 -0
- package/dist/border/geo-border.d.ts.map +1 -0
- package/dist/border/geo-border.js +234 -0
- package/dist/border/geo-border.js.map +1 -0
- package/dist/border/geometry.d.ts +40 -0
- package/dist/border/geometry.d.ts.map +1 -0
- package/dist/border/geometry.js +382 -0
- package/dist/border/geometry.js.map +1 -0
- package/dist/border/index.d.ts +15 -0
- package/dist/border/index.d.ts.map +1 -0
- package/dist/border/index.js +15 -0
- package/dist/border/index.js.map +1 -0
- package/dist/border/loader.d.ts +31 -0
- package/dist/border/loader.d.ts.map +1 -0
- package/dist/border/loader.js +87 -0
- package/dist/border/loader.js.map +1 -0
- package/dist/border/registry.d.ts +64 -0
- package/dist/border/registry.d.ts.map +1 -0
- package/dist/border/registry.js +152 -0
- package/dist/border/registry.js.map +1 -0
- package/dist/border/selection.d.ts +21 -0
- package/dist/border/selection.d.ts.map +1 -0
- package/dist/border/selection.js +86 -0
- package/dist/border/selection.js.map +1 -0
- package/dist/border/simplify.d.ts +12 -0
- package/dist/border/simplify.d.ts.map +1 -0
- package/dist/border/simplify.js +116 -0
- package/dist/border/simplify.js.map +1 -0
- package/dist/browser/index.js +25 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manifest.json +11 -0
- package/dist/globe/atmosphere.d.ts +38 -0
- package/dist/globe/atmosphere.d.ts.map +1 -0
- package/dist/globe/atmosphere.js +91 -0
- package/dist/globe/atmosphere.js.map +1 -0
- package/dist/globe/borders.d.ts +39 -0
- package/dist/globe/borders.d.ts.map +1 -0
- package/dist/globe/borders.js +116 -0
- package/dist/globe/borders.js.map +1 -0
- package/dist/globe/data/ne-110m.d.ts +25 -0
- package/dist/globe/data/ne-110m.d.ts.map +1 -0
- package/dist/globe/data/ne-110m.js +198 -0
- package/dist/globe/data/ne-110m.js.map +1 -0
- package/dist/globe/globe-places.d.ts +140 -0
- package/dist/globe/globe-places.d.ts.map +1 -0
- package/dist/globe/globe-places.js +262 -0
- package/dist/globe/globe-places.js.map +1 -0
- package/dist/globe/globe.d.ts +193 -0
- package/dist/globe/globe.d.ts.map +1 -0
- package/dist/globe/globe.js +435 -0
- package/dist/globe/globe.js.map +1 -0
- package/dist/globe/index.d.ts +17 -0
- package/dist/globe/index.d.ts.map +1 -0
- package/dist/globe/index.js +17 -0
- package/dist/globe/index.js.map +1 -0
- package/dist/globe/places.d.ts +69 -0
- package/dist/globe/places.d.ts.map +1 -0
- package/dist/globe/places.js +94 -0
- package/dist/globe/places.js.map +1 -0
- package/dist/globe/projection.d.ts +182 -0
- package/dist/globe/projection.d.ts.map +1 -0
- package/dist/globe/projection.js +215 -0
- package/dist/globe/projection.js.map +1 -0
- package/dist/globe/world-map.d.ts +83 -0
- package/dist/globe/world-map.d.ts.map +1 -0
- package/dist/globe/world-map.js +169 -0
- package/dist/globe/world-map.js.map +1 -0
- package/dist/globe/world.d.ts +46 -0
- package/dist/globe/world.d.ts.map +1 -0
- package/dist/globe/world.js +71 -0
- package/dist/globe/world.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes.d.ts +19 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +19 -0
- package/dist/nodes.js.map +1 -0
- package/package.json +69 -3
- package/registry.json +31 -0
- package/src/border/format.ts +147 -0
- package/src/border/geo-border.ts +260 -0
- package/src/border/geometry.ts +463 -0
- package/src/border/index.ts +14 -0
- package/src/border/loader.ts +97 -0
- package/src/border/registry.ts +191 -0
- package/src/border/selection.ts +95 -0
- package/src/border/simplify.ts +110 -0
- package/src/globe/atmosphere.ts +98 -0
- package/src/globe/borders.ts +166 -0
- package/src/globe/data/ne-110m.ts +214 -0
- package/src/globe/globe-places.ts +352 -0
- package/src/globe/globe.ts +552 -0
- package/src/globe/index.ts +16 -0
- package/src/globe/places.ts +132 -0
- package/src/globe/projection.ts +261 -0
- package/src/globe/world-map.ts +227 -0
- package/src/globe/world.ts +111 -0
- package/src/index.ts +5 -0
- package/src/nodes.ts +19 -0
- package/README.md +0 -4
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The world as a flat drawing — the one `Graphics2D` that becomes the sphere's
|
|
3
|
+
* texture.
|
|
4
|
+
*
|
|
5
|
+
* Baking the map in 2D rather than tessellating it in 3D is the node's central
|
|
6
|
+
* decision, and it is not a shortcut. A WebGL line ignores any width above one
|
|
7
|
+
* pixel (motion-script says so outright on `LineStroke3D`), so borders drawn as
|
|
8
|
+
* 3D lines are hairlines at every zoom and every resolution, and the only way to
|
|
9
|
+
* thicken one is to sweep a tube along it — three hundred tubes, for a picture
|
|
10
|
+
* that is flat. Drawn here, a border is an ordinary stroke: any weight, any
|
|
11
|
+
* colour, joined and antialiased by Skia, and it costs one rasterize rather than
|
|
12
|
+
* one per frame.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is therefore *style*, and the node's cache key is exactly the
|
|
15
|
+
* arguments below. Nothing that moves — a marker, an arc, the camera — is drawn
|
|
16
|
+
* into this, because baking something that moves would put every frame of its
|
|
17
|
+
* animation through a full re-rasterize.
|
|
18
|
+
*
|
|
19
|
+
* ## One path per country, and this is the whole performance story
|
|
20
|
+
*
|
|
21
|
+
* The obvious shape is one `path` op holding all 289 rings — the nonzero fill
|
|
22
|
+
* rule sorts out what is land, and it is one op instead of 177. **Do not do
|
|
23
|
+
* that.** Skia's cost for filling and stroking a single path is badly
|
|
24
|
+
* superlinear in its *contour* count, and the difference is not marginal:
|
|
25
|
+
*
|
|
26
|
+
* one path, 289 contours, 10,649 commands 10,691 ms
|
|
27
|
+
* one path per country (177 of them) 205 ms
|
|
28
|
+
*
|
|
29
|
+
* Measured in the browser, on the same data, at the same resolution — a 52×
|
|
30
|
+
* difference, and the reason adding a Globe to a scene used to lock the app for
|
|
31
|
+
* ten seconds. It is also almost independent of the texture's resolution (256²
|
|
32
|
+
* and 2048² are within 10% of each other), which is the tell that the cost is
|
|
33
|
+
* geometry, not pixels, and that turning the map down would not have saved it.
|
|
34
|
+
*
|
|
35
|
+
* Splitting per country is safe for the fill: each country's rings — exterior
|
|
36
|
+
* and holes together — stay in *its* path, so the nonzero rule still cuts
|
|
37
|
+
* Lesotho out of South Africa, and Lesotho's own path paints it back.
|
|
38
|
+
*
|
|
39
|
+
* ## The lines are a separate pass, and have to be
|
|
40
|
+
*
|
|
41
|
+
* The country paths are filled and **not** stroked. Stroking them would draw
|
|
42
|
+
* every international border twice — once from each side, since both countries
|
|
43
|
+
* carry it — and a doubly-composited antialiased fringe reads as a line half
|
|
44
|
+
* again as wide as the coastlines beside it. `borders.ts` deduplicates the
|
|
45
|
+
* segments and stitches them into runs; this file strokes those instead, so
|
|
46
|
+
* every line on the map is drawn exactly once at exactly one weight.
|
|
47
|
+
*/
|
|
48
|
+
import { Graphics2D } from "@motionscript/sdk";
|
|
49
|
+
import { canvasBounds, latLonToCanvas } from "./projection.js";
|
|
50
|
+
import { borderLines } from "./borders.js";
|
|
51
|
+
import { world } from "./world.js";
|
|
52
|
+
/** The buffer's height for a given width — equirectangular is always 2:1. */
|
|
53
|
+
export function mapHeight(width) {
|
|
54
|
+
return Math.round(width / 2);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The map, drawn.
|
|
58
|
+
*
|
|
59
|
+
* **Hoist the result.** motion-script identifies a surface texture by its source
|
|
60
|
+
* object, so a fresh `Graphics2D` every frame misses the raster cache, redraws
|
|
61
|
+
* ten thousand paths, reads the buffer back off the GPU and uploads it again —
|
|
62
|
+
* sixty times a second, to produce the same image. `Globe` holds one against
|
|
63
|
+
* this style; nothing else should call this.
|
|
64
|
+
*/
|
|
65
|
+
export function worldMapGraphics(style) {
|
|
66
|
+
const width = style.width;
|
|
67
|
+
const height = mapHeight(width);
|
|
68
|
+
const bounds = canvasBounds(width, height);
|
|
69
|
+
const g = new Graphics2D();
|
|
70
|
+
// The ocean is the whole buffer rather than a sphere-coloured backdrop,
|
|
71
|
+
// because the texture has to be opaque: the sphere is lit, and a transparent
|
|
72
|
+
// texel would show the inside of the far side through it.
|
|
73
|
+
g.rect({ width, height }).fill(style.ocean);
|
|
74
|
+
// Land, filled and **not** stroked — see `drawBoundaries` for where the lines
|
|
75
|
+
// come from and why they cannot come from here.
|
|
76
|
+
//
|
|
77
|
+
// One path per country, not one for the world: see the module note for what
|
|
78
|
+
// that costs.
|
|
79
|
+
for (const country of world().countries) {
|
|
80
|
+
const commands = [];
|
|
81
|
+
for (const ring of country.rings)
|
|
82
|
+
appendRing(commands, ring, width, height);
|
|
83
|
+
if (commands.length === 0)
|
|
84
|
+
continue;
|
|
85
|
+
g.path({ data: commands, centerBounds: bounds }).fill(style.land);
|
|
86
|
+
}
|
|
87
|
+
if (style.borderWidth > 0)
|
|
88
|
+
drawBoundaries(g, style, width, height, bounds);
|
|
89
|
+
// Last, and over the land. The graticule is reference rather than art
|
|
90
|
+
// direction — the same reading the Canvas 3D viewport's grid takes — so it
|
|
91
|
+
// reads as an overlay on whatever it crosses rather than as terrain.
|
|
92
|
+
if (style.graticule) {
|
|
93
|
+
for (const line of graticuleLines(style.graticuleStep, width, height)) {
|
|
94
|
+
g.line({ points: line }).stroke({
|
|
95
|
+
fill: style.graticuleColor,
|
|
96
|
+
weight: style.graticuleWidth,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return g;
|
|
101
|
+
}
|
|
102
|
+
function appendRing(commands, ring, width, height) {
|
|
103
|
+
const count = ring.points.length / 2;
|
|
104
|
+
for (let i = 0; i < count; i++) {
|
|
105
|
+
const { x, y } = latLonToCanvas({ lon: ring.points[i * 2], lat: ring.points[i * 2 + 1] }, width, height);
|
|
106
|
+
commands.push(i === 0 ? { type: "M", x, y } : { type: "L", x, y });
|
|
107
|
+
}
|
|
108
|
+
// Closed explicitly: the ring's repeated last point was dropped when it was
|
|
109
|
+
// vendored, so without this the stroke leaves a notch at every country's
|
|
110
|
+
// starting vertex and the fill's last edge is implied rather than drawn.
|
|
111
|
+
commands.push({ type: "Z" });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every coast and border, stroked once each.
|
|
115
|
+
*
|
|
116
|
+
* **One path per run**, for exactly the reason the countries are one path each:
|
|
117
|
+
* a path's stroke cost is superlinear in its contour count. Gathering the 277
|
|
118
|
+
* runs into two paths of ~200 contours put this straight back on the cliff the
|
|
119
|
+
* countries were moved off — measured at 2.5 s of stall, against none at one
|
|
120
|
+
* run per path.
|
|
121
|
+
*
|
|
122
|
+
* One-per-run rather than a tuned chunk size because there is no number to
|
|
123
|
+
* defend: 277 paths is the same order as the 177 the countries already use, and
|
|
124
|
+
* a constant here would be a knob whose safe range nobody could state.
|
|
125
|
+
*/
|
|
126
|
+
function drawBoundaries(g, style, width, height, bounds) {
|
|
127
|
+
const paint = { fill: style.border, weight: style.borderWidth };
|
|
128
|
+
for (const line of borderLines()) {
|
|
129
|
+
const count = line.length / 2;
|
|
130
|
+
if (count < 2)
|
|
131
|
+
continue;
|
|
132
|
+
const commands = [];
|
|
133
|
+
for (let i = 0; i < count; i++) {
|
|
134
|
+
const { x, y } = latLonToCanvas({ lon: line[i * 2], lat: line[i * 2 + 1] }, width, height);
|
|
135
|
+
commands.push(i === 0 ? { type: "M", x, y } : { type: "L", x, y });
|
|
136
|
+
}
|
|
137
|
+
// Left open: a boundary run is a line, not a ring, and closing it would draw
|
|
138
|
+
// a chord back to wherever the run happened to start.
|
|
139
|
+
g.path({ data: commands, centerBounds: bounds }).stroke(paint);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The meridians and parallels, as polylines in drawing space.
|
|
144
|
+
*
|
|
145
|
+
* Straight lines, because an equirectangular projection is exactly the one where
|
|
146
|
+
* meridians and parallels are straight — the grid is curved on the sphere only
|
|
147
|
+
* because the sphere curves. Meridians run pole to pole and parallels edge to
|
|
148
|
+
* edge, and both stop short of the poles by one step so the drawing does not
|
|
149
|
+
* pile every meridian onto the same two points.
|
|
150
|
+
*/
|
|
151
|
+
function graticuleLines(step, width, height) {
|
|
152
|
+
const spacing = Math.max(5, Math.min(90, step));
|
|
153
|
+
const lines = [];
|
|
154
|
+
for (let lon = -180; lon <= 180; lon += spacing) {
|
|
155
|
+
const meridian = [];
|
|
156
|
+
for (let lat = -90; lat <= 90; lat += 5) {
|
|
157
|
+
meridian.push(latLonToCanvas({ lat, lon }, width, height));
|
|
158
|
+
}
|
|
159
|
+
lines.push(meridian);
|
|
160
|
+
}
|
|
161
|
+
for (let lat = -90 + spacing; lat <= 90 - spacing; lat += spacing) {
|
|
162
|
+
lines.push([
|
|
163
|
+
latLonToCanvas({ lat, lon: -180 }, width, height),
|
|
164
|
+
latLonToCanvas({ lat, lon: 180 }, width, height),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=world-map.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world-map.js","sourceRoot":"","sources":["../../src/globe/world-map.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The world as a flat drawing — the one `Graphics2D` that becomes the sphere's\n * texture.\n *\n * Baking the map in 2D rather than tessellating it in 3D is the node's central\n * decision, and it is not a shortcut. A WebGL line ignores any width above one\n * pixel (motion-script says so outright on `LineStroke3D`), so borders drawn as\n * 3D lines are hairlines at every zoom and every resolution, and the only way to\n * thicken one is to sweep a tube along it — three hundred tubes, for a picture\n * that is flat. Drawn here, a border is an ordinary stroke: any weight, any\n * colour, joined and antialiased by Skia, and it costs one rasterize rather than\n * one per frame.\n *\n * Everything here is therefore *style*, and the node's cache key is exactly the\n * arguments below. Nothing that moves — a marker, an arc, the camera — is drawn\n * into this, because baking something that moves would put every frame of its\n * animation through a full re-rasterize.\n *\n * ## One path per country, and this is the whole performance story\n *\n * The obvious shape is one `path` op holding all 289 rings — the nonzero fill\n * rule sorts out what is land, and it is one op instead of 177. **Do not do\n * that.** Skia's cost for filling and stroking a single path is badly\n * superlinear in its *contour* count, and the difference is not marginal:\n *\n * one path, 289 contours, 10,649 commands 10,691 ms\n * one path per country (177 of them) 205 ms\n *\n * Measured in the browser, on the same data, at the same resolution — a 52×\n * difference, and the reason adding a Globe to a scene used to lock the app for\n * ten seconds. It is also almost independent of the texture's resolution (256²\n * and 2048² are within 10% of each other), which is the tell that the cost is\n * geometry, not pixels, and that turning the map down would not have saved it.\n *\n * Splitting per country is safe for the fill: each country's rings — exterior\n * and holes together — stay in *its* path, so the nonzero rule still cuts\n * Lesotho out of South Africa, and Lesotho's own path paints it back.\n *\n * ## The lines are a separate pass, and have to be\n *\n * The country paths are filled and **not** stroked. Stroking them would draw\n * every international border twice — once from each side, since both countries\n * carry it — and a doubly-composited antialiased fringe reads as a line half\n * again as wide as the coastlines beside it. `borders.ts` deduplicates the\n * segments and stitches them into runs; this file strokes those instead, so\n * every line on the map is drawn exactly once at exactly one weight.\n */\n\nimport { Graphics2D, type PathCommand } from \"@motionscript/sdk\"\n\nimport { canvasBounds, latLonToCanvas } from \"./projection\"\nimport { borderLines } from \"./borders\"\nimport { world, type Ring } from \"./world\"\n\n/** Everything that changes the baked pixels. Also the node's cache key. */\nexport interface WorldMapStyle {\n /** Buffer width in pixels. Height is half of it — the equirectangular ratio. */\n width: number\n ocean: string\n land: string\n /**\n * The colour of every coast and border.\n *\n * Alpha is fine here, and that is worth saying because it briefly was not:\n * while the country paths were stroked, a shared border was drawn twice and a\n * translucent one came out at double density. `drawBoundaries` draws each line\n * exactly once, so the colour means what it says.\n */\n border: string\n borderWidth: number\n graticule: boolean\n graticuleColor: string\n /** Degrees between graticule lines. */\n graticuleStep: number\n graticuleWidth: number\n}\n\n/** The buffer's height for a given width — equirectangular is always 2:1. */\nexport function mapHeight(width: number): number {\n return Math.round(width / 2)\n}\n\n/**\n * The map, drawn.\n *\n * **Hoist the result.** motion-script identifies a surface texture by its source\n * object, so a fresh `Graphics2D` every frame misses the raster cache, redraws\n * ten thousand paths, reads the buffer back off the GPU and uploads it again —\n * sixty times a second, to produce the same image. `Globe` holds one against\n * this style; nothing else should call this.\n */\nexport function worldMapGraphics(style: WorldMapStyle): Graphics2D {\n const width = style.width\n const height = mapHeight(width)\n const bounds = canvasBounds(width, height)\n\n const g = new Graphics2D()\n\n // The ocean is the whole buffer rather than a sphere-coloured backdrop,\n // because the texture has to be opaque: the sphere is lit, and a transparent\n // texel would show the inside of the far side through it.\n g.rect({ width, height }).fill(style.ocean)\n\n // Land, filled and **not** stroked — see `drawBoundaries` for where the lines\n // come from and why they cannot come from here.\n //\n // One path per country, not one for the world: see the module note for what\n // that costs.\n for (const country of world().countries) {\n const commands: PathCommand[] = []\n for (const ring of country.rings) appendRing(commands, ring, width, height)\n if (commands.length === 0) continue\n g.path({ data: commands, centerBounds: bounds }).fill(style.land)\n }\n\n if (style.borderWidth > 0) drawBoundaries(g, style, width, height, bounds)\n\n // Last, and over the land. The graticule is reference rather than art\n // direction — the same reading the Canvas 3D viewport's grid takes — so it\n // reads as an overlay on whatever it crosses rather than as terrain.\n if (style.graticule) {\n for (const line of graticuleLines(style.graticuleStep, width, height)) {\n g.line({ points: line }).stroke({\n fill: style.graticuleColor,\n weight: style.graticuleWidth,\n })\n }\n }\n\n return g\n}\n\nfunction appendRing(\n commands: PathCommand[],\n ring: Ring,\n width: number,\n height: number\n): void {\n const count = ring.points.length / 2\n for (let i = 0; i < count; i++) {\n const { x, y } = latLonToCanvas(\n { lon: ring.points[i * 2]!, lat: ring.points[i * 2 + 1]! },\n width,\n height\n )\n commands.push(i === 0 ? { type: \"M\", x, y } : { type: \"L\", x, y })\n }\n // Closed explicitly: the ring's repeated last point was dropped when it was\n // vendored, so without this the stroke leaves a notch at every country's\n // starting vertex and the fill's last edge is implied rather than drawn.\n commands.push({ type: \"Z\" })\n}\n\n/**\n * Every coast and border, stroked once each.\n *\n * **One path per run**, for exactly the reason the countries are one path each:\n * a path's stroke cost is superlinear in its contour count. Gathering the 277\n * runs into two paths of ~200 contours put this straight back on the cliff the\n * countries were moved off — measured at 2.5 s of stall, against none at one\n * run per path.\n *\n * One-per-run rather than a tuned chunk size because there is no number to\n * defend: 277 paths is the same order as the 177 the countries already use, and\n * a constant here would be a knob whose safe range nobody could state.\n */\nfunction drawBoundaries(\n g: Graphics2D,\n style: WorldMapStyle,\n width: number,\n height: number,\n bounds: readonly [number, number, number, number]\n): void {\n const paint = { fill: style.border, weight: style.borderWidth }\n\n for (const line of borderLines()) {\n const count = line.length / 2\n if (count < 2) continue\n const commands: PathCommand[] = []\n for (let i = 0; i < count; i++) {\n const { x, y } = latLonToCanvas(\n { lon: line[i * 2]!, lat: line[i * 2 + 1]! },\n width,\n height\n )\n commands.push(i === 0 ? { type: \"M\", x, y } : { type: \"L\", x, y })\n }\n // Left open: a boundary run is a line, not a ring, and closing it would draw\n // a chord back to wherever the run happened to start.\n g.path({ data: commands, centerBounds: bounds }).stroke(paint)\n }\n}\n\n/**\n * The meridians and parallels, as polylines in drawing space.\n *\n * Straight lines, because an equirectangular projection is exactly the one where\n * meridians and parallels are straight — the grid is curved on the sphere only\n * because the sphere curves. Meridians run pole to pole and parallels edge to\n * edge, and both stop short of the poles by one step so the drawing does not\n * pile every meridian onto the same two points.\n */\nfunction graticuleLines(\n step: number,\n width: number,\n height: number\n): { x: number; y: number }[][] {\n const spacing = Math.max(5, Math.min(90, step))\n const lines: { x: number; y: number }[][] = []\n\n for (let lon = -180; lon <= 180; lon += spacing) {\n const meridian: { x: number; y: number }[] = []\n for (let lat = -90; lat <= 90; lat += 5) {\n meridian.push(latLonToCanvas({ lat, lon }, width, height))\n }\n lines.push(meridian)\n }\n\n for (let lat = -90 + spacing; lat <= 90 - spacing; lat += spacing) {\n lines.push([\n latLonToCanvas({ lat, lon: -180 }, width, height),\n latLonToCanvas({ lat, lon: 180 }, width, height),\n ])\n }\n\n return lines\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vendored boundaries, decoded once and held.
|
|
3
|
+
*
|
|
4
|
+
* The generated module beside it is a string and a table of ring sizes; this
|
|
5
|
+
* turns them into flat coordinate arrays and memoises the result, so a document
|
|
6
|
+
* with a globe pays for the decode once per process and a document without one
|
|
7
|
+
* never pays at all. That laziness is the whole reason the data can be a plain
|
|
8
|
+
* static import rather than something threaded through a resolver: the 40 KB is
|
|
9
|
+
* transferred either way, but nothing is parsed, allocated or walked until a
|
|
10
|
+
* globe actually draws.
|
|
11
|
+
*
|
|
12
|
+
* Coordinates stay in **degrees**, not pixels and not sphere positions. The
|
|
13
|
+
* texture resolution is a prop and the projection is `projection.ts`'s business;
|
|
14
|
+
* baking either into the data would mean re-vendoring to change a number.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* One closed ring, as `[lon, lat, lon, lat, …]` degrees.
|
|
18
|
+
*
|
|
19
|
+
* Flat rather than an array of pairs for the reason the Protein's structure is
|
|
20
|
+
* flat: the map is redrawn whenever its style changes and a pair object per
|
|
21
|
+
* vertex would put ten thousand allocations behind every colour edit.
|
|
22
|
+
*/
|
|
23
|
+
export interface Ring {
|
|
24
|
+
points: Float64Array;
|
|
25
|
+
/**
|
|
26
|
+
* Whether this ring cuts rather than fills.
|
|
27
|
+
*
|
|
28
|
+
* It is drawn either way — the nonzero fill rule does the cutting, off the
|
|
29
|
+
* winding the vendoring script enforced. This is here so a caller that wants
|
|
30
|
+
* to *outline* only the coastlines can skip it.
|
|
31
|
+
*/
|
|
32
|
+
hole: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface Country {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
rings: Ring[];
|
|
38
|
+
}
|
|
39
|
+
export interface World {
|
|
40
|
+
countries: readonly Country[];
|
|
41
|
+
/** Every ring of every country, in one list — the order the map draws in. */
|
|
42
|
+
rings: readonly Ring[];
|
|
43
|
+
}
|
|
44
|
+
/** The world, decoded on first use and shared thereafter. */
|
|
45
|
+
export declare function world(): World;
|
|
46
|
+
//# sourceMappingURL=world.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/globe/world.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,MAAM,EAAE,YAAY,CAAA;IACpB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,IAAI,EAAE,CAAA;CACd;AAED,MAAM,WAAW,KAAK;IACpB,SAAS,EAAE,SAAS,OAAO,EAAE,CAAA;IAC7B,6EAA6E;IAC7E,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;CACvB;AAID,6DAA6D;AAC7D,wBAAgB,KAAK,IAAI,KAAK,CAE7B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vendored boundaries, decoded once and held.
|
|
3
|
+
*
|
|
4
|
+
* The generated module beside it is a string and a table of ring sizes; this
|
|
5
|
+
* turns them into flat coordinate arrays and memoises the result, so a document
|
|
6
|
+
* with a globe pays for the decode once per process and a document without one
|
|
7
|
+
* never pays at all. That laziness is the whole reason the data can be a plain
|
|
8
|
+
* static import rather than something threaded through a resolver: the 40 KB is
|
|
9
|
+
* transferred either way, but nothing is parsed, allocated or walked until a
|
|
10
|
+
* globe actually draws.
|
|
11
|
+
*
|
|
12
|
+
* Coordinates stay in **degrees**, not pixels and not sphere positions. The
|
|
13
|
+
* texture resolution is a prop and the projection is `projection.ts`'s business;
|
|
14
|
+
* baking either into the data would mean re-vendoring to change a number.
|
|
15
|
+
*/
|
|
16
|
+
import { COUNTRIES, POINTS, PRECISION } from "./data/ne-110m.js";
|
|
17
|
+
let decoded = null;
|
|
18
|
+
/** The world, decoded on first use and shared thereafter. */
|
|
19
|
+
export function world() {
|
|
20
|
+
return (decoded ??= decode());
|
|
21
|
+
}
|
|
22
|
+
function decode() {
|
|
23
|
+
const countries = [];
|
|
24
|
+
const rings = [];
|
|
25
|
+
const cursor = { at: 0 };
|
|
26
|
+
for (const row of COUNTRIES) {
|
|
27
|
+
const own = [];
|
|
28
|
+
for (const size of row.rings) {
|
|
29
|
+
const ring = { points: readRing(cursor, Math.abs(size)), hole: size < 0 };
|
|
30
|
+
own.push(ring);
|
|
31
|
+
rings.push(ring);
|
|
32
|
+
}
|
|
33
|
+
countries.push({ id: row.id, name: row.name, rings: own });
|
|
34
|
+
}
|
|
35
|
+
// A short read leaves the tail unconsumed and every ring after the mistake
|
|
36
|
+
// shifted, which draws as a plausible-looking but wrong world rather than as
|
|
37
|
+
// an error — so the two halves of the generated file are checked against each
|
|
38
|
+
// other here, once, where the cost is nothing.
|
|
39
|
+
if (cursor.at !== POINTS.length) {
|
|
40
|
+
throw new Error(`globe: boundary data is inconsistent — decoded ${cursor.at} of ${POINTS.length} characters`);
|
|
41
|
+
}
|
|
42
|
+
return { countries, rings };
|
|
43
|
+
}
|
|
44
|
+
/** `count` points, delta-decoded from the ring's own origin. */
|
|
45
|
+
function readRing(cursor, count) {
|
|
46
|
+
const points = new Float64Array(count * 2);
|
|
47
|
+
let lon = 0;
|
|
48
|
+
let lat = 0;
|
|
49
|
+
for (let i = 0; i < count; i++) {
|
|
50
|
+
lon += readValue(cursor);
|
|
51
|
+
lat += readValue(cursor);
|
|
52
|
+
points[i * 2] = lon / PRECISION;
|
|
53
|
+
points[i * 2 + 1] = lat / PRECISION;
|
|
54
|
+
}
|
|
55
|
+
return points;
|
|
56
|
+
}
|
|
57
|
+
/** One zig-zagged varint. The inverse of the script's `encodeValue`. */
|
|
58
|
+
function readValue(cursor) {
|
|
59
|
+
let shift = 0;
|
|
60
|
+
let result = 0;
|
|
61
|
+
for (;;) {
|
|
62
|
+
const byte = POINTS.charCodeAt(cursor.at++) - 63;
|
|
63
|
+
result |= (byte & 0x1f) << shift;
|
|
64
|
+
// The continuation bit: set on every chunk but the last.
|
|
65
|
+
if (byte < 0x20)
|
|
66
|
+
break;
|
|
67
|
+
shift += 5;
|
|
68
|
+
}
|
|
69
|
+
return result & 1 ? ~(result >> 1) : result >> 1;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=world.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/globe/world.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The vendored boundaries, decoded once and held.\n *\n * The generated module beside it is a string and a table of ring sizes; this\n * turns them into flat coordinate arrays and memoises the result, so a document\n * with a globe pays for the decode once per process and a document without one\n * never pays at all. That laziness is the whole reason the data can be a plain\n * static import rather than something threaded through a resolver: the 40 KB is\n * transferred either way, but nothing is parsed, allocated or walked until a\n * globe actually draws.\n *\n * Coordinates stay in **degrees**, not pixels and not sphere positions. The\n * texture resolution is a prop and the projection is `projection.ts`'s business;\n * baking either into the data would mean re-vendoring to change a number.\n */\n\nimport { COUNTRIES, POINTS, PRECISION, type CountryRow } from \"./data/ne-110m\"\n\n/**\n * One closed ring, as `[lon, lat, lon, lat, …]` degrees.\n *\n * Flat rather than an array of pairs for the reason the Protein's structure is\n * flat: the map is redrawn whenever its style changes and a pair object per\n * vertex would put ten thousand allocations behind every colour edit.\n */\nexport interface Ring {\n points: Float64Array\n /**\n * Whether this ring cuts rather than fills.\n *\n * It is drawn either way — the nonzero fill rule does the cutting, off the\n * winding the vendoring script enforced. This is here so a caller that wants\n * to *outline* only the coastlines can skip it.\n */\n hole: boolean\n}\n\nexport interface Country {\n id: string\n name: string\n rings: Ring[]\n}\n\nexport interface World {\n countries: readonly Country[]\n /** Every ring of every country, in one list — the order the map draws in. */\n rings: readonly Ring[]\n}\n\nlet decoded: World | null = null\n\n/** The world, decoded on first use and shared thereafter. */\nexport function world(): World {\n return (decoded ??= decode())\n}\n\nfunction decode(): World {\n const countries: Country[] = []\n const rings: Ring[] = []\n\n const cursor = { at: 0 }\n for (const row of COUNTRIES as readonly CountryRow[]) {\n const own: Ring[] = []\n for (const size of row.rings) {\n const ring = { points: readRing(cursor, Math.abs(size)), hole: size < 0 }\n own.push(ring)\n rings.push(ring)\n }\n countries.push({ id: row.id, name: row.name, rings: own })\n }\n\n // A short read leaves the tail unconsumed and every ring after the mistake\n // shifted, which draws as a plausible-looking but wrong world rather than as\n // an error — so the two halves of the generated file are checked against each\n // other here, once, where the cost is nothing.\n if (cursor.at !== POINTS.length) {\n throw new Error(\n `globe: boundary data is inconsistent — decoded ${cursor.at} of ${POINTS.length} characters`\n )\n }\n\n return { countries, rings }\n}\n\n/** `count` points, delta-decoded from the ring's own origin. */\nfunction readRing(cursor: { at: number }, count: number): Float64Array {\n const points = new Float64Array(count * 2)\n let lon = 0\n let lat = 0\n for (let i = 0; i < count; i++) {\n lon += readValue(cursor)\n lat += readValue(cursor)\n points[i * 2] = lon / PRECISION\n points[i * 2 + 1] = lat / PRECISION\n }\n return points\n}\n\n/** One zig-zagged varint. The inverse of the script's `encodeValue`. */\nfunction readValue(cursor: { at: number }): number {\n let shift = 0\n let result = 0\n for (;;) {\n const byte = POINTS.charCodeAt(cursor.at++) - 63\n result |= (byte & 0x1f) << shift\n // The continuation bit: set on every chunk but the last.\n if (byte < 0x20) break\n shift += 5\n }\n return result & 1 ? ~(result >> 1) : result >> 1\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AAExB,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./border/index.js";
|
|
2
|
+
export * from "./globe/index.js";
|
|
3
|
+
// Not through ./globe: that barrel is copied with the globe item, and world.ts is not.
|
|
4
|
+
export * from "./globe/world.js";
|
|
5
|
+
export { NODES } from "./nodes.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,uFAAuF;AACvF,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC","sourcesContent":["export * from \"./border\";\nexport * from \"./globe\";\n// Not through ./globe: that barrel is copied with the globe item, and world.ts is not.\nexport * from \"./globe/world\";\nexport { NODES } from \"./nodes\";\n"]}
|
package/dist/nodes.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GeoBorder } from "./border/index.js";
|
|
2
|
+
import { Globe } from "./globe/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every node type this package publishes.
|
|
5
|
+
*
|
|
6
|
+
* A host registers these by handing the array to an engine —
|
|
7
|
+
* `new Engine(platform, { nodes: NODES })` — which reads each class's `@node()`
|
|
8
|
+
* key. The decorator only *declares* that key: nothing is registered by the mere
|
|
9
|
+
* act of importing a module, so a registry holds exactly what its owner asked
|
|
10
|
+
* for and two engines in one process can differ about it.
|
|
11
|
+
*
|
|
12
|
+
* Listing class **values** is also what survives bundling. This package declares
|
|
13
|
+
* `sideEffects: false`, which licenses a bundler to drop a module nothing
|
|
14
|
+
* imports a value from, and a document names a node type by string — so
|
|
15
|
+
* `import "./globe/index.js"` for its side effect is exactly what gets shaken out, where an
|
|
16
|
+
* array of bindings is a data dependency that cannot be.
|
|
17
|
+
*/
|
|
18
|
+
export declare const NODES: (typeof GeoBorder | typeof Globe)[];
|
|
19
|
+
//# sourceMappingURL=nodes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,KAAK,qCAAqB,CAAC"}
|
package/dist/nodes.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GeoBorder } from "./border/index.js";
|
|
2
|
+
import { Globe } from "./globe/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every node type this package publishes.
|
|
5
|
+
*
|
|
6
|
+
* A host registers these by handing the array to an engine —
|
|
7
|
+
* `new Engine(platform, { nodes: NODES })` — which reads each class's `@node()`
|
|
8
|
+
* key. The decorator only *declares* that key: nothing is registered by the mere
|
|
9
|
+
* act of importing a module, so a registry holds exactly what its owner asked
|
|
10
|
+
* for and two engines in one process can differ about it.
|
|
11
|
+
*
|
|
12
|
+
* Listing class **values** is also what survives bundling. This package declares
|
|
13
|
+
* `sideEffects: false`, which licenses a bundler to drop a module nothing
|
|
14
|
+
* imports a value from, and a document names a node type by string — so
|
|
15
|
+
* `import "./globe/index.js"` for its side effect is exactly what gets shaken out, where an
|
|
16
|
+
* array of bindings is a data dependency that cannot be.
|
|
17
|
+
*/
|
|
18
|
+
export const NODES = [Globe, GeoBorder];
|
|
19
|
+
//# sourceMappingURL=nodes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nodes.js","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC","sourcesContent":["import { GeoBorder } from \"./border\";\nimport { Globe } from \"./globe\";\n\n/**\n * Every node type this package publishes.\n *\n * A host registers these by handing the array to an engine —\n * `new Engine(platform, { nodes: NODES })` — which reads each class's `@node()`\n * key. The decorator only *declares* that key: nothing is registered by the mere\n * act of importing a module, so a registry holds exactly what its owner asked\n * for and two engines in one process can differ about it.\n *\n * Listing class **values** is also what survives bundling. This package declares\n * `sideEffects: false`, which licenses a bundler to drop a module nothing\n * imports a value from, and a document names a node type by string — so\n * `import \"./globe\"` for its side effect is exactly what gets shaken out, where an\n * array of bindings is a data dependency that cannot be.\n */\nexport const NODES = [Globe, GeoBorder];\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionscript/geo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Geography for Motion Script: an animated globe, and country and state outlines by ISO 3166 code.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/motionscript-dev/motionscript.git",
|
|
9
|
+
"directory": "packages/components/geo"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://motionscript.dev",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/motionscript-dev/motionscript/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"registry.json",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"!dist/**/*.tsbuildinfo",
|
|
24
|
+
"!src/**/tests/**",
|
|
25
|
+
"!src/**/*.test.ts",
|
|
26
|
+
"!src/**/*.test.tsx",
|
|
27
|
+
"!src/**/*.fixtures.ts"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"motion-script",
|
|
40
|
+
"motion script",
|
|
41
|
+
"geo",
|
|
42
|
+
"animation"
|
|
43
|
+
],
|
|
44
|
+
"license": "Apache-2.0",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@motionscript/sdk": "^0.1.0-alpha.3"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/d3-geo": "^3.1.0",
|
|
50
|
+
"@types/node": "^25.6.2",
|
|
51
|
+
"eslint": "^9.39.4",
|
|
52
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
53
|
+
"eslint-plugin-import-x": "^4.16.1",
|
|
54
|
+
"tsc-alias": "^1.8.16",
|
|
55
|
+
"typescript": "^6.0.3",
|
|
56
|
+
"typescript-eslint": "^8.48.0",
|
|
57
|
+
"vitest": "^4.1.5",
|
|
58
|
+
"@motionscript/core": "0.1.0-alpha.3",
|
|
59
|
+
"@motionscript/sdk": "0.1.0-alpha.3"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"d3-geo": "^3.1.1"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths && node ../../../scripts/build-browser.mjs",
|
|
66
|
+
"dev": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --resolve-full-paths -w",
|
|
67
|
+
"clean": "rimraf --glob dist .turbo *.tsbuildinfo",
|
|
68
|
+
"test": "vitest --passWithNoTests",
|
|
69
|
+
"lint": "eslint .",
|
|
70
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
71
|
+
}
|
|
6
72
|
}
|
package/registry.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kit": {
|
|
3
|
+
"barrel": "src/index.ts",
|
|
4
|
+
"specifier": "@motionscript/geo"
|
|
5
|
+
},
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"name": "globe",
|
|
9
|
+
"description": "A shaded globe with borders, graticules, markers and great-circle arcs.",
|
|
10
|
+
"root": "src/globe",
|
|
11
|
+
"files": [
|
|
12
|
+
"atmosphere.ts",
|
|
13
|
+
"borders.ts",
|
|
14
|
+
"globe-places.ts",
|
|
15
|
+
"globe.ts",
|
|
16
|
+
"index.ts",
|
|
17
|
+
"places.ts",
|
|
18
|
+
"projection.ts",
|
|
19
|
+
"world-map.ts"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "geo-border",
|
|
24
|
+
"description": "Country and state outlines by ISO 3166 code, with adjustable detail.",
|
|
25
|
+
"root": "src/border",
|
|
26
|
+
"files": [
|
|
27
|
+
"geo-border.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|