@motionscript/geo 0.0.0-stage → 0.1.0-alpha.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/dist/border/format.d.ts +73 -0
  4. package/dist/border/format.d.ts.map +1 -0
  5. package/dist/border/format.js +68 -0
  6. package/dist/border/format.js.map +1 -0
  7. package/dist/border/geo-border.d.ts +44 -0
  8. package/dist/border/geo-border.d.ts.map +1 -0
  9. package/dist/border/geo-border.js +234 -0
  10. package/dist/border/geo-border.js.map +1 -0
  11. package/dist/border/geometry.d.ts +40 -0
  12. package/dist/border/geometry.d.ts.map +1 -0
  13. package/dist/border/geometry.js +382 -0
  14. package/dist/border/geometry.js.map +1 -0
  15. package/dist/border/index.d.ts +15 -0
  16. package/dist/border/index.d.ts.map +1 -0
  17. package/dist/border/index.js +15 -0
  18. package/dist/border/index.js.map +1 -0
  19. package/dist/border/loader.d.ts +31 -0
  20. package/dist/border/loader.d.ts.map +1 -0
  21. package/dist/border/loader.js +87 -0
  22. package/dist/border/loader.js.map +1 -0
  23. package/dist/border/registry.d.ts +64 -0
  24. package/dist/border/registry.d.ts.map +1 -0
  25. package/dist/border/registry.js +152 -0
  26. package/dist/border/registry.js.map +1 -0
  27. package/dist/border/selection.d.ts +21 -0
  28. package/dist/border/selection.d.ts.map +1 -0
  29. package/dist/border/selection.js +86 -0
  30. package/dist/border/selection.js.map +1 -0
  31. package/dist/border/simplify.d.ts +12 -0
  32. package/dist/border/simplify.d.ts.map +1 -0
  33. package/dist/border/simplify.js +116 -0
  34. package/dist/border/simplify.js.map +1 -0
  35. package/dist/browser/index.js +25 -0
  36. package/dist/browser/index.js.map +7 -0
  37. package/dist/browser/manifest.json +11 -0
  38. package/dist/globe/atmosphere.d.ts +38 -0
  39. package/dist/globe/atmosphere.d.ts.map +1 -0
  40. package/dist/globe/atmosphere.js +91 -0
  41. package/dist/globe/atmosphere.js.map +1 -0
  42. package/dist/globe/borders.d.ts +39 -0
  43. package/dist/globe/borders.d.ts.map +1 -0
  44. package/dist/globe/borders.js +116 -0
  45. package/dist/globe/borders.js.map +1 -0
  46. package/dist/globe/data/ne-110m.d.ts +25 -0
  47. package/dist/globe/data/ne-110m.d.ts.map +1 -0
  48. package/dist/globe/data/ne-110m.js +198 -0
  49. package/dist/globe/data/ne-110m.js.map +1 -0
  50. package/dist/globe/globe-places.d.ts +140 -0
  51. package/dist/globe/globe-places.d.ts.map +1 -0
  52. package/dist/globe/globe-places.js +262 -0
  53. package/dist/globe/globe-places.js.map +1 -0
  54. package/dist/globe/globe.d.ts +193 -0
  55. package/dist/globe/globe.d.ts.map +1 -0
  56. package/dist/globe/globe.js +435 -0
  57. package/dist/globe/globe.js.map +1 -0
  58. package/dist/globe/index.d.ts +17 -0
  59. package/dist/globe/index.d.ts.map +1 -0
  60. package/dist/globe/index.js +17 -0
  61. package/dist/globe/index.js.map +1 -0
  62. package/dist/globe/places.d.ts +69 -0
  63. package/dist/globe/places.d.ts.map +1 -0
  64. package/dist/globe/places.js +94 -0
  65. package/dist/globe/places.js.map +1 -0
  66. package/dist/globe/projection.d.ts +182 -0
  67. package/dist/globe/projection.d.ts.map +1 -0
  68. package/dist/globe/projection.js +215 -0
  69. package/dist/globe/projection.js.map +1 -0
  70. package/dist/globe/world-map.d.ts +83 -0
  71. package/dist/globe/world-map.d.ts.map +1 -0
  72. package/dist/globe/world-map.js +169 -0
  73. package/dist/globe/world-map.js.map +1 -0
  74. package/dist/globe/world.d.ts +46 -0
  75. package/dist/globe/world.d.ts.map +1 -0
  76. package/dist/globe/world.js +71 -0
  77. package/dist/globe/world.js.map +1 -0
  78. package/dist/index.d.ts +5 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +6 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/nodes.d.ts +19 -0
  83. package/dist/nodes.d.ts.map +1 -0
  84. package/dist/nodes.js +19 -0
  85. package/dist/nodes.js.map +1 -0
  86. package/package.json +68 -3
  87. package/registry.json +31 -0
  88. package/src/border/format.ts +147 -0
  89. package/src/border/geo-border.ts +260 -0
  90. package/src/border/geometry.ts +463 -0
  91. package/src/border/index.ts +14 -0
  92. package/src/border/loader.ts +97 -0
  93. package/src/border/registry.ts +191 -0
  94. package/src/border/selection.ts +95 -0
  95. package/src/border/simplify.ts +110 -0
  96. package/src/globe/atmosphere.ts +98 -0
  97. package/src/globe/borders.ts +166 -0
  98. package/src/globe/data/ne-110m.ts +214 -0
  99. package/src/globe/globe-places.ts +352 -0
  100. package/src/globe/globe.ts +552 -0
  101. package/src/globe/index.ts +16 -0
  102. package/src/globe/places.ts +132 -0
  103. package/src/globe/projection.ts +261 -0
  104. package/src/globe/world-map.ts +227 -0
  105. package/src/globe/world.ts +111 -0
  106. package/src/index.ts +5 -0
  107. package/src/nodes.ts +19 -0
  108. package/README.md +0 -4
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The marker and arc lists, resolved into the things a frame draws.
3
+ *
4
+ * The authored shape and its serialization live in `model/globe-places.ts`,
5
+ * shared with the inspector's tiles. What is here is the other half: turning a
6
+ * pinned arc end into a coordinate, dropping what cannot draw, and deciding how
7
+ * finely a tube is swept. None of it is meaningful to the panel, and all of it
8
+ * runs on every rebuild — which is why the two halves are separate modules
9
+ * rather than one with a flag.
10
+ */
11
+ import { arcsOf, markersOf, markerColorAt, } from "./globe-places.js";
12
+ /**
13
+ * The markers a frame draws, in list order.
14
+ *
15
+ * The palette is applied **here** rather than left to the drawing code, so the
16
+ * one place that decides what colour an uncoloured marker is also the place the
17
+ * swatch in the panel reads — see `markerColorAt`. Position in the list is the
18
+ * palette index, which is why a reorder recolours: the list is a ranking, and
19
+ * the first place being the loudest is the point.
20
+ */
21
+ export function drawableMarkers(value) {
22
+ return markersOf(value).flatMap((marker, index) => marker.enabled && marker.opacity > 0
23
+ ? [
24
+ {
25
+ id: marker.id,
26
+ lat: marker.lat,
27
+ lon: marker.lon,
28
+ color: marker.color === "" ? markerColorAt(index) : marker.color,
29
+ size: clamp(marker.size, 0.001, 0.5),
30
+ opacity: marker.opacity,
31
+ },
32
+ ]
33
+ : []);
34
+ }
35
+ /**
36
+ * The arcs a frame draws, with pinned ends looked up in the marker list.
37
+ *
38
+ * An end pinned to a marker that is gone drops the whole arc rather than falling
39
+ * back to the origin. A line to null island is a picture of something that is
40
+ * not true, and it is *harder* to notice than a missing one: an arc that
41
+ * vanishes when its endpoint is deleted reads as a consequence, where one that
42
+ * swings to the Gulf of Guinea reads as a bug in the projection.
43
+ *
44
+ * A **disabled** marker still anchors an arc, deliberately. Switching a dot off
45
+ * is about the dot; an arc that also wanted to go is switched off itself.
46
+ */
47
+ export function drawableArcs(arcsValue, markersValue) {
48
+ const byId = new Map();
49
+ for (const marker of markersOf(markersValue))
50
+ byId.set(marker.id, marker);
51
+ return arcsOf(arcsValue).flatMap((arc) => {
52
+ if (!arc.enabled || arc.progress <= 0)
53
+ return [];
54
+ const from = resolveEnd(arc.from, byId);
55
+ const to = resolveEnd(arc.to, byId);
56
+ if (!from || !to)
57
+ return [];
58
+ return [
59
+ {
60
+ id: arc.id,
61
+ from,
62
+ to,
63
+ color: arc.color,
64
+ width: clamp(arc.width, 0.0005, 0.1),
65
+ lift: arc.lift,
66
+ progress: arc.progress,
67
+ },
68
+ ];
69
+ });
70
+ }
71
+ function resolveEnd(end, markers) {
72
+ if (end.kind === "place")
73
+ return { lat: end.lat, lon: end.lon };
74
+ const marker = markers.get(end.markerId);
75
+ return marker ? { lat: marker.lat, lon: marker.lon } : null;
76
+ }
77
+ /**
78
+ * How many segments an arc's tube is swept along.
79
+ *
80
+ * Proportional to how far it goes, so a hop between neighbours is not paying for
81
+ * a pole-to-pole path's resolution and a long haul does not visibly chord. The
82
+ * floor keeps a very short arc from degenerating into a two-point tube, which
83
+ * three cannot frame.
84
+ */
85
+ export function arcSegments(arc) {
86
+ const from = arc.from;
87
+ const to = arc.to;
88
+ const span = Math.abs(to.lat - from.lat) + Math.abs(to.lon - from.lon);
89
+ return Math.max(8, Math.min(128, Math.round(span * 0.7)));
90
+ }
91
+ function clamp(value, min, max) {
92
+ return value < min ? min : value > max ? max : value;
93
+ }
94
+ //# sourceMappingURL=places.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"places.js","sourceRoot":"","sources":["../../src/globe/places.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The marker and arc lists, resolved into the things a frame draws.\n *\n * The authored shape and its serialization live in `model/globe-places.ts`,\n * shared with the inspector's tiles. What is here is the other half: turning a\n * pinned arc end into a coordinate, dropping what cannot draw, and deciding how\n * finely a tube is swept. None of it is meaningful to the panel, and all of it\n * runs on every rebuild — which is why the two halves are separate modules\n * rather than one with a flag.\n */\n\nimport {\n arcsOf,\n markersOf,\n markerColorAt,\n type ArcEnd,\n type GlobeArc,\n type GlobeMarker,\n} from \"./globe-places\"\n\n/** A marker with everything the render needs decided. */\nexport interface DrawableMarker {\n id: string\n lat: number\n lon: number\n color: string\n size: number\n opacity: number\n}\n\n/** An arc with both ends resolved to real coordinates. */\nexport interface DrawableArc {\n id: string\n from: { lat: number; lon: number }\n to: { lat: number; lon: number }\n color: string\n width: number\n lift: number\n progress: number\n}\n\n/**\n * The markers a frame draws, in list order.\n *\n * The palette is applied **here** rather than left to the drawing code, so the\n * one place that decides what colour an uncoloured marker is also the place the\n * swatch in the panel reads — see `markerColorAt`. Position in the list is the\n * palette index, which is why a reorder recolours: the list is a ranking, and\n * the first place being the loudest is the point.\n */\nexport function drawableMarkers(value: string): DrawableMarker[] {\n return markersOf(value).flatMap((marker, index) =>\n marker.enabled && marker.opacity > 0\n ? [\n {\n id: marker.id,\n lat: marker.lat,\n lon: marker.lon,\n color: marker.color === \"\" ? markerColorAt(index) : marker.color,\n size: clamp(marker.size, 0.001, 0.5),\n opacity: marker.opacity,\n },\n ]\n : []\n )\n}\n\n/**\n * The arcs a frame draws, with pinned ends looked up in the marker list.\n *\n * An end pinned to a marker that is gone drops the whole arc rather than falling\n * back to the origin. A line to null island is a picture of something that is\n * not true, and it is *harder* to notice than a missing one: an arc that\n * vanishes when its endpoint is deleted reads as a consequence, where one that\n * swings to the Gulf of Guinea reads as a bug in the projection.\n *\n * A **disabled** marker still anchors an arc, deliberately. Switching a dot off\n * is about the dot; an arc that also wanted to go is switched off itself.\n */\nexport function drawableArcs(\n arcsValue: string,\n markersValue: string\n): DrawableArc[] {\n const byId = new Map<string, GlobeMarker>()\n for (const marker of markersOf(markersValue)) byId.set(marker.id, marker)\n\n return arcsOf(arcsValue).flatMap((arc) => {\n if (!arc.enabled || arc.progress <= 0) return []\n const from = resolveEnd(arc.from, byId)\n const to = resolveEnd(arc.to, byId)\n if (!from || !to) return []\n return [\n {\n id: arc.id,\n from,\n to,\n color: arc.color,\n width: clamp(arc.width, 0.0005, 0.1),\n lift: arc.lift,\n progress: arc.progress,\n },\n ]\n })\n}\n\nfunction resolveEnd(\n end: ArcEnd,\n markers: Map<string, GlobeMarker>\n): { lat: number; lon: number } | null {\n if (end.kind === \"place\") return { lat: end.lat, lon: end.lon }\n const marker = markers.get(end.markerId)\n return marker ? { lat: marker.lat, lon: marker.lon } : null\n}\n\n/**\n * How many segments an arc's tube is swept along.\n *\n * Proportional to how far it goes, so a hop between neighbours is not paying for\n * a pole-to-pole path's resolution and a long haul does not visibly chord. The\n * floor keeps a very short arc from degenerating into a two-point tube, which\n * three cannot frame.\n */\nexport function arcSegments(arc: Pick<GlobeArc | DrawableArc, \"from\" | \"to\">): number {\n const from = arc.from as { lat: number; lon: number }\n const to = arc.to as { lat: number; lon: number }\n const span = Math.abs(to.lat - from.lat) + Math.abs(to.lon - from.lon)\n return Math.max(8, Math.min(128, Math.round(span * 0.7)))\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value\n}\n"]}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Where a latitude and a longitude are, in the three spaces the Globe works in:
3
+ * the unit sphere it draws on, the equirectangular texture it paints, and the
4
+ * orbit camera it is looked at through.
5
+ *
6
+ * All three are fixed by one decision, and it is the decision every other file
7
+ * in this node depends on, so it is made here and nowhere else.
8
+ *
9
+ * ## The convention, and why it is not the obvious one
10
+ *
11
+ * Two things have to be true at once, and motion-script's camera lets you have
12
+ * either but not both by default:
13
+ *
14
+ * 1. **The stored `orbit` field reads directly as a longitude.** That would let
15
+ * the camera be three appearance numbers *and* the Fly To command be
16
+ * arithmetic on two of them with nothing in between.
17
+ * 2. **East is to the right.** Looking at the Atlantic from space, Africa is
18
+ * right of Brazil. A globe that fails this is a mirrored Earth, which no
19
+ * amount of convenience elsewhere pays for.
20
+ *
21
+ * The clash: `resolveCameraPlacement` puts a camera at azimuth `a` at
22
+ * `(sin a, ·, cos a)`, and `cameraOrbit` (`view3d-kit/shared.ts`) feeds it
23
+ * `90 - orbit`, so a camera at stored orbit θ sits at `(cos θ, ·, sin θ)` and its
24
+ * screen-right is `-Z`. Increasing θ therefore swings the scene **rightward** —
25
+ * measured, not reasoned: it is what every existing 3D node does, and the globe
26
+ * has to match or it is the one viewport whose drag goes backwards.
27
+ *
28
+ * So (1) is what gives. Longitude runs the *other* way round the Y axis —
29
+ * {@link latLonToVector} negates Z, which is what puts east on the right — and a
30
+ * camera at heading θ is consequently centred over longitude −θ. The stored
31
+ * field is a heading, labelled "Orbit" like every other viewport's, and
32
+ * {@link orbitForLongitude} is the one place the two are converted.
33
+ *
34
+ * The Fly To command is where that conversion earns its keep: its arguments
35
+ * genuinely are a latitude and a longitude, and it turns them into a heading
36
+ * here rather than anywhere else.
37
+ *
38
+ * ## The texture
39
+ *
40
+ * Plain equirectangular, the orientation any world map image already has:
41
+ * longitude −180…180 left to right, latitude +90…−90 top to bottom. That falls
42
+ * out of the convention above rather than being chosen — see {@link latLonToUV},
43
+ * which derives it from three's own sphere generator — and it is the strongest
44
+ * evidence the convention is right: get the handedness wrong and the map that
45
+ * wraps correctly is a mirrored one.
46
+ */
47
+ /** A point on the unit sphere. */
48
+ export interface Vec3 {
49
+ x: number;
50
+ y: number;
51
+ z: number;
52
+ }
53
+ /** A position on the globe, in degrees. */
54
+ export interface LatLon {
55
+ /** Degrees north of the equator, −90…90. */
56
+ lat: number;
57
+ /** Degrees east of the prime meridian, −180…180. */
58
+ lon: number;
59
+ }
60
+ /**
61
+ * The bounding box every country path is centred against.
62
+ *
63
+ * A `Path` centres on its *own* bounding box unless it is given one, so drawing
64
+ * the world as one path per country and leaving this off stacks all 177 of them
65
+ * on the origin. Passing the same frame to each is what keeps them in place —
66
+ * the same reason the LaTeX renderer passes whole-formula bounds to every glyph.
67
+ */
68
+ export declare const WORLD_BOUNDS: readonly [-180, -90, 180, 90];
69
+ /**
70
+ * The unit-sphere direction of a latitude and longitude.
71
+ *
72
+ * `z` is negated against the textbook spherical formula, which is the whole of
73
+ * the convention above: it is what puts east on the right.
74
+ */
75
+ export declare function latLonToVector({ lat, lon }: LatLon): Vec3;
76
+ /** {@link latLonToVector}, scaled off the surface — markers, arc endpoints. */
77
+ export declare function latLonToPosition(at: LatLon, radius: number): [number, number, number];
78
+ /**
79
+ * Where a latitude and longitude land in the equirectangular texture, as `uv`
80
+ * in `[0,1]²`.
81
+ *
82
+ * Derived rather than chosen, from the two pieces of machinery underneath:
83
+ *
84
+ * - three's `SphereGeometry` puts vertex `u` at `(-cos 2πu, ·, sin 2πu)` and
85
+ * writes `uv = (u, 1 - v)`, with `v = 0` at the north pole.
86
+ * - motion-script uploads a rasterized surface with `flipY = true`
87
+ * (`skia-render/src/three/handlers/texture.ts`), so the buffer's **first row
88
+ * is `uv.y = 1`**.
89
+ *
90
+ * Solving `(-cos 2πu, sin 2πu)` against {@link latLonToVector}'s `(x, z)` gives
91
+ * `2πu = π + λ`, and the latitude falls straight out of `y = cos θ`. The result
92
+ * is the ordinary world-map layout, which is the point.
93
+ */
94
+ export declare function latLonToUV({ lat, lon }: LatLon): {
95
+ u: number;
96
+ v: number;
97
+ };
98
+ /**
99
+ * Where a latitude and longitude land in the **drawing space** of the baked
100
+ * texture: pixels, **y upward**, origin at the buffer's centre.
101
+ *
102
+ * Two of motion-script's conventions meet here, and neither is guessable from a
103
+ * picture that comes out wrong, so both are written down:
104
+ *
105
+ * - **The origin is the buffer's centre**, because `SkiaRenderContext.rasterize`
106
+ * translates the offscreen canvas by half its size before handing it to the
107
+ * source. Drawn from a top-left origin the map lands a quarter of the world
108
+ * off the edge — and still wraps onto the sphere without complaint.
109
+ * - **`y` is up.** A `PathCommand[]`, and a line's `points`, are authored y-up
110
+ * and mirrored by the backend on the way to Skia (`flipPathY`, and the
111
+ * `points` map beside it in `render-context.ts`). Drawn y-down the world is
112
+ * simply upside down — and every continent still has its own shape, so it
113
+ * reads as a texture flipped somewhere in the pipeline rather than as a sign
114
+ * in the one function that decides.
115
+ *
116
+ * Deliberately *not* the raw buffer's own row order. The backend's flip lands
117
+ * north on the first row, and `flipY` on the uploaded texture maps that row to
118
+ * `uv.y = 1` — which is where {@link latLonToUV} independently puts the north
119
+ * pole. A test ties the two together rather than trusting them to agree.
120
+ */
121
+ export declare function latLonToCanvas({ lat, lon }: LatLon, width: number, height: number): {
122
+ x: number;
123
+ y: number;
124
+ };
125
+ /**
126
+ * The frame every country path is centred against, in that same space.
127
+ *
128
+ * See {@link WORLD_BOUNDS} for why passing it to each path matters.
129
+ */
130
+ export declare function canvasBounds(width: number, height: number): [number, number, number, number];
131
+ /**
132
+ * The stored `orbit` heading that puts `lon` at the centre of the frame.
133
+ *
134
+ * **A negation, and it is the price of two things that matter more than the
135
+ * field reading as a longitude.**
136
+ *
137
+ * `orbit` has to behave exactly as it does on every other 3D node: the canvas
138
+ * drag adds to it (`orbitByDrag`), and increasing it must swing the scene the
139
+ * same way it swings a molecule or a surface, or the globe is the one viewport
140
+ * whose drag goes backwards. That pins `orbit` to `cameraOrbit` with no
141
+ * conversion in between.
142
+ *
143
+ * East also has to be on the right, which pins {@link latLonToVector}'s sign.
144
+ *
145
+ * Those two together force the third: a camera at heading θ is centred over
146
+ * longitude −θ. So the stored field is a **heading**, not a longitude — which is
147
+ * why the panel labels it "Orbit" like every other viewport's, and why the Fly
148
+ * To command, whose arguments genuinely are a latitude and a longitude,
149
+ * converts through here.
150
+ */
151
+ export declare function orbitForLongitude(lon: number): number;
152
+ /** The longitude a stored `orbit` heading is centred over. Its own inverse. */
153
+ export declare function longitudeForOrbit(orbit: number): number;
154
+ /**
155
+ * Points along the great circle from `from` to `to`, `segments` spans of them.
156
+ *
157
+ * Spherical linear interpolation rather than a lerp of the two lat/lons: the
158
+ * latter is a rhumb line, which crosses the antimeridian the long way and bulges
159
+ * badly at high latitudes — a "London to Tokyo" arc drawn that way runs through
160
+ * Kazakhstan instead of over the pole, which is the one thing the picture exists
161
+ * to show.
162
+ *
163
+ * `lift` raises the middle of the arc off the surface, as a fraction of the
164
+ * radius, on a sine so both ends meet the sphere flush. Zero draws the path on
165
+ * the ground.
166
+ */
167
+ export declare function greatCircle(from: LatLon, to: LatLon, options?: {
168
+ radius?: number;
169
+ lift?: number;
170
+ segments?: number;
171
+ }): [number, number, number][];
172
+ /**
173
+ * The signed turn from one heading to another, taking whichever way round is
174
+ * shorter — so 170° to −170° is a 20° nudge east rather than 340° west.
175
+ *
176
+ * The same arithmetic `camera-kit`'s unused `shortestTurn` states; it is
177
+ * duplicated rather than imported because that one is private to a module this
178
+ * package's 3D nodes do not otherwise depend on, and because a Fly To that got
179
+ * this wrong would be the command's single most visible failure.
180
+ */
181
+ export declare function shortestTurn(from: number, to: number): number;
182
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/globe/projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,kCAAkC;AAClC,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,2CAA2C;AAC3C,MAAM,WAAW,MAAM;IACrB,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAA;IACX,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,+BAAgC,CAAA;AAIzD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,MAAM,GAAG,IAAI,CASzD;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,GACb,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAG1B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAEzE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAC5B,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAE1B;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAElC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAkC5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7D"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Where a latitude and a longitude are, in the three spaces the Globe works in:
3
+ * the unit sphere it draws on, the equirectangular texture it paints, and the
4
+ * orbit camera it is looked at through.
5
+ *
6
+ * All three are fixed by one decision, and it is the decision every other file
7
+ * in this node depends on, so it is made here and nowhere else.
8
+ *
9
+ * ## The convention, and why it is not the obvious one
10
+ *
11
+ * Two things have to be true at once, and motion-script's camera lets you have
12
+ * either but not both by default:
13
+ *
14
+ * 1. **The stored `orbit` field reads directly as a longitude.** That would let
15
+ * the camera be three appearance numbers *and* the Fly To command be
16
+ * arithmetic on two of them with nothing in between.
17
+ * 2. **East is to the right.** Looking at the Atlantic from space, Africa is
18
+ * right of Brazil. A globe that fails this is a mirrored Earth, which no
19
+ * amount of convenience elsewhere pays for.
20
+ *
21
+ * The clash: `resolveCameraPlacement` puts a camera at azimuth `a` at
22
+ * `(sin a, ·, cos a)`, and `cameraOrbit` (`view3d-kit/shared.ts`) feeds it
23
+ * `90 - orbit`, so a camera at stored orbit θ sits at `(cos θ, ·, sin θ)` and its
24
+ * screen-right is `-Z`. Increasing θ therefore swings the scene **rightward** —
25
+ * measured, not reasoned: it is what every existing 3D node does, and the globe
26
+ * has to match or it is the one viewport whose drag goes backwards.
27
+ *
28
+ * So (1) is what gives. Longitude runs the *other* way round the Y axis —
29
+ * {@link latLonToVector} negates Z, which is what puts east on the right — and a
30
+ * camera at heading θ is consequently centred over longitude −θ. The stored
31
+ * field is a heading, labelled "Orbit" like every other viewport's, and
32
+ * {@link orbitForLongitude} is the one place the two are converted.
33
+ *
34
+ * The Fly To command is where that conversion earns its keep: its arguments
35
+ * genuinely are a latitude and a longitude, and it turns them into a heading
36
+ * here rather than anywhere else.
37
+ *
38
+ * ## The texture
39
+ *
40
+ * Plain equirectangular, the orientation any world map image already has:
41
+ * longitude −180…180 left to right, latitude +90…−90 top to bottom. That falls
42
+ * out of the convention above rather than being chosen — see {@link latLonToUV},
43
+ * which derives it from three's own sphere generator — and it is the strongest
44
+ * evidence the convention is right: get the handedness wrong and the map that
45
+ * wraps correctly is a mirrored one.
46
+ */
47
+ /**
48
+ * The bounding box every country path is centred against.
49
+ *
50
+ * A `Path` centres on its *own* bounding box unless it is given one, so drawing
51
+ * the world as one path per country and leaving this off stacks all 177 of them
52
+ * on the origin. Passing the same frame to each is what keeps them in place —
53
+ * the same reason the LaTeX renderer passes whole-formula bounds to every glyph.
54
+ */
55
+ export const WORLD_BOUNDS = [-180, -90, 180, 90];
56
+ const DEG = Math.PI / 180;
57
+ /**
58
+ * The unit-sphere direction of a latitude and longitude.
59
+ *
60
+ * `z` is negated against the textbook spherical formula, which is the whole of
61
+ * the convention above: it is what puts east on the right.
62
+ */
63
+ export function latLonToVector({ lat, lon }) {
64
+ const phi = lat * DEG;
65
+ const lambda = lon * DEG;
66
+ const ring = Math.cos(phi);
67
+ return {
68
+ x: ring * Math.cos(lambda),
69
+ y: Math.sin(phi),
70
+ z: -ring * Math.sin(lambda),
71
+ };
72
+ }
73
+ /** {@link latLonToVector}, scaled off the surface — markers, arc endpoints. */
74
+ export function latLonToPosition(at, radius) {
75
+ const { x, y, z } = latLonToVector(at);
76
+ return [x * radius, y * radius, z * radius];
77
+ }
78
+ /**
79
+ * Where a latitude and longitude land in the equirectangular texture, as `uv`
80
+ * in `[0,1]²`.
81
+ *
82
+ * Derived rather than chosen, from the two pieces of machinery underneath:
83
+ *
84
+ * - three's `SphereGeometry` puts vertex `u` at `(-cos 2πu, ·, sin 2πu)` and
85
+ * writes `uv = (u, 1 - v)`, with `v = 0` at the north pole.
86
+ * - motion-script uploads a rasterized surface with `flipY = true`
87
+ * (`skia-render/src/three/handlers/texture.ts`), so the buffer's **first row
88
+ * is `uv.y = 1`**.
89
+ *
90
+ * Solving `(-cos 2πu, sin 2πu)` against {@link latLonToVector}'s `(x, z)` gives
91
+ * `2πu = π + λ`, and the latitude falls straight out of `y = cos θ`. The result
92
+ * is the ordinary world-map layout, which is the point.
93
+ */
94
+ export function latLonToUV({ lat, lon }) {
95
+ return { u: (lon + 180) / 360, v: 0.5 + lat / 180 };
96
+ }
97
+ /**
98
+ * Where a latitude and longitude land in the **drawing space** of the baked
99
+ * texture: pixels, **y upward**, origin at the buffer's centre.
100
+ *
101
+ * Two of motion-script's conventions meet here, and neither is guessable from a
102
+ * picture that comes out wrong, so both are written down:
103
+ *
104
+ * - **The origin is the buffer's centre**, because `SkiaRenderContext.rasterize`
105
+ * translates the offscreen canvas by half its size before handing it to the
106
+ * source. Drawn from a top-left origin the map lands a quarter of the world
107
+ * off the edge — and still wraps onto the sphere without complaint.
108
+ * - **`y` is up.** A `PathCommand[]`, and a line's `points`, are authored y-up
109
+ * and mirrored by the backend on the way to Skia (`flipPathY`, and the
110
+ * `points` map beside it in `render-context.ts`). Drawn y-down the world is
111
+ * simply upside down — and every continent still has its own shape, so it
112
+ * reads as a texture flipped somewhere in the pipeline rather than as a sign
113
+ * in the one function that decides.
114
+ *
115
+ * Deliberately *not* the raw buffer's own row order. The backend's flip lands
116
+ * north on the first row, and `flipY` on the uploaded texture maps that row to
117
+ * `uv.y = 1` — which is where {@link latLonToUV} independently puts the north
118
+ * pole. A test ties the two together rather than trusting them to agree.
119
+ */
120
+ export function latLonToCanvas({ lat, lon }, width, height) {
121
+ return { x: (lon / 360) * width, y: (lat / 180) * height };
122
+ }
123
+ /**
124
+ * The frame every country path is centred against, in that same space.
125
+ *
126
+ * See {@link WORLD_BOUNDS} for why passing it to each path matters.
127
+ */
128
+ export function canvasBounds(width, height) {
129
+ return [-width / 2, -height / 2, width / 2, height / 2];
130
+ }
131
+ /**
132
+ * The stored `orbit` heading that puts `lon` at the centre of the frame.
133
+ *
134
+ * **A negation, and it is the price of two things that matter more than the
135
+ * field reading as a longitude.**
136
+ *
137
+ * `orbit` has to behave exactly as it does on every other 3D node: the canvas
138
+ * drag adds to it (`orbitByDrag`), and increasing it must swing the scene the
139
+ * same way it swings a molecule or a surface, or the globe is the one viewport
140
+ * whose drag goes backwards. That pins `orbit` to `cameraOrbit` with no
141
+ * conversion in between.
142
+ *
143
+ * East also has to be on the right, which pins {@link latLonToVector}'s sign.
144
+ *
145
+ * Those two together force the third: a camera at heading θ is centred over
146
+ * longitude −θ. So the stored field is a **heading**, not a longitude — which is
147
+ * why the panel labels it "Orbit" like every other viewport's, and why the Fly
148
+ * To command, whose arguments genuinely are a latitude and a longitude,
149
+ * converts through here.
150
+ */
151
+ export function orbitForLongitude(lon) {
152
+ return -lon;
153
+ }
154
+ /** The longitude a stored `orbit` heading is centred over. Its own inverse. */
155
+ export function longitudeForOrbit(orbit) {
156
+ return -orbit;
157
+ }
158
+ /**
159
+ * Points along the great circle from `from` to `to`, `segments` spans of them.
160
+ *
161
+ * Spherical linear interpolation rather than a lerp of the two lat/lons: the
162
+ * latter is a rhumb line, which crosses the antimeridian the long way and bulges
163
+ * badly at high latitudes — a "London to Tokyo" arc drawn that way runs through
164
+ * Kazakhstan instead of over the pole, which is the one thing the picture exists
165
+ * to show.
166
+ *
167
+ * `lift` raises the middle of the arc off the surface, as a fraction of the
168
+ * radius, on a sine so both ends meet the sphere flush. Zero draws the path on
169
+ * the ground.
170
+ */
171
+ export function greatCircle(from, to, options = {}) {
172
+ const radius = options.radius ?? 1;
173
+ const lift = options.lift ?? 0;
174
+ const segments = Math.max(1, Math.floor(options.segments ?? 64));
175
+ const a = latLonToVector(from);
176
+ const b = latLonToVector(to);
177
+ const dot = clamp(a.x * b.x + a.y * b.y + a.z * b.z, -1, 1);
178
+ const omega = Math.acos(dot);
179
+ const points = [];
180
+ for (let i = 0; i <= segments; i++) {
181
+ const t = i / segments;
182
+ // Antipodal or coincident endpoints leave the plane of the circle
183
+ // undefined; a straight blend is the honest answer for both, and
184
+ // renormalising below puts it back on the sphere either way.
185
+ const [wa, wb] = Math.abs(omega) < 1e-6
186
+ ? [1 - t, t]
187
+ : [
188
+ Math.sin((1 - t) * omega) / Math.sin(omega),
189
+ Math.sin(t * omega) / Math.sin(omega),
190
+ ];
191
+ const x = a.x * wa + b.x * wb;
192
+ const y = a.y * wa + b.y * wb;
193
+ const z = a.z * wa + b.z * wb;
194
+ const length = Math.hypot(x, y, z) || 1;
195
+ const scale = (radius * (1 + lift * Math.sin(Math.PI * t))) / length;
196
+ points.push([x * scale, y * scale, z * scale]);
197
+ }
198
+ return points;
199
+ }
200
+ /**
201
+ * The signed turn from one heading to another, taking whichever way round is
202
+ * shorter — so 170° to −170° is a 20° nudge east rather than 340° west.
203
+ *
204
+ * The same arithmetic `camera-kit`'s unused `shortestTurn` states; it is
205
+ * duplicated rather than imported because that one is private to a module this
206
+ * package's 3D nodes do not otherwise depend on, and because a Fly To that got
207
+ * this wrong would be the command's single most visible failure.
208
+ */
209
+ export function shortestTurn(from, to) {
210
+ return ((((to - from) % 360) + 540) % 360) - 180;
211
+ }
212
+ function clamp(value, min, max) {
213
+ return value < min ? min : value > max ? max : value;
214
+ }
215
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection.js","sourceRoot":"","sources":["../../src/globe/projection.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where a latitude and a longitude are, in the three spaces the Globe works in:\n * the unit sphere it draws on, the equirectangular texture it paints, and the\n * orbit camera it is looked at through.\n *\n * All three are fixed by one decision, and it is the decision every other file\n * in this node depends on, so it is made here and nowhere else.\n *\n * ## The convention, and why it is not the obvious one\n *\n * Two things have to be true at once, and motion-script's camera lets you have\n * either but not both by default:\n *\n * 1. **The stored `orbit` field reads directly as a longitude.** That would let\n * the camera be three appearance numbers *and* the Fly To command be\n * arithmetic on two of them with nothing in between.\n * 2. **East is to the right.** Looking at the Atlantic from space, Africa is\n * right of Brazil. A globe that fails this is a mirrored Earth, which no\n * amount of convenience elsewhere pays for.\n *\n * The clash: `resolveCameraPlacement` puts a camera at azimuth `a` at\n * `(sin a, ·, cos a)`, and `cameraOrbit` (`view3d-kit/shared.ts`) feeds it\n * `90 - orbit`, so a camera at stored orbit θ sits at `(cos θ, ·, sin θ)` and its\n * screen-right is `-Z`. Increasing θ therefore swings the scene **rightward** —\n * measured, not reasoned: it is what every existing 3D node does, and the globe\n * has to match or it is the one viewport whose drag goes backwards.\n *\n * So (1) is what gives. Longitude runs the *other* way round the Y axis —\n * {@link latLonToVector} negates Z, which is what puts east on the right — and a\n * camera at heading θ is consequently centred over longitude −θ. The stored\n * field is a heading, labelled \"Orbit\" like every other viewport's, and\n * {@link orbitForLongitude} is the one place the two are converted.\n *\n * The Fly To command is where that conversion earns its keep: its arguments\n * genuinely are a latitude and a longitude, and it turns them into a heading\n * here rather than anywhere else.\n *\n * ## The texture\n *\n * Plain equirectangular, the orientation any world map image already has:\n * longitude −180…180 left to right, latitude +90…−90 top to bottom. That falls\n * out of the convention above rather than being chosen — see {@link latLonToUV},\n * which derives it from three's own sphere generator — and it is the strongest\n * evidence the convention is right: get the handedness wrong and the map that\n * wraps correctly is a mirrored one.\n */\n\n/** A point on the unit sphere. */\nexport interface Vec3 {\n x: number\n y: number\n z: number\n}\n\n/** A position on the globe, in degrees. */\nexport interface LatLon {\n /** Degrees north of the equator, −90…90. */\n lat: number\n /** Degrees east of the prime meridian, −180…180. */\n lon: number\n}\n\n/**\n * The bounding box every country path is centred against.\n *\n * A `Path` centres on its *own* bounding box unless it is given one, so drawing\n * the world as one path per country and leaving this off stacks all 177 of them\n * on the origin. Passing the same frame to each is what keeps them in place —\n * the same reason the LaTeX renderer passes whole-formula bounds to every glyph.\n */\nexport const WORLD_BOUNDS = [-180, -90, 180, 90] as const\n\nconst DEG = Math.PI / 180\n\n/**\n * The unit-sphere direction of a latitude and longitude.\n *\n * `z` is negated against the textbook spherical formula, which is the whole of\n * the convention above: it is what puts east on the right.\n */\nexport function latLonToVector({ lat, lon }: LatLon): Vec3 {\n const phi = lat * DEG\n const lambda = lon * DEG\n const ring = Math.cos(phi)\n return {\n x: ring * Math.cos(lambda),\n y: Math.sin(phi),\n z: -ring * Math.sin(lambda),\n }\n}\n\n/** {@link latLonToVector}, scaled off the surface — markers, arc endpoints. */\nexport function latLonToPosition(\n at: LatLon,\n radius: number\n): [number, number, number] {\n const { x, y, z } = latLonToVector(at)\n return [x * radius, y * radius, z * radius]\n}\n\n/**\n * Where a latitude and longitude land in the equirectangular texture, as `uv`\n * in `[0,1]²`.\n *\n * Derived rather than chosen, from the two pieces of machinery underneath:\n *\n * - three's `SphereGeometry` puts vertex `u` at `(-cos 2πu, ·, sin 2πu)` and\n * writes `uv = (u, 1 - v)`, with `v = 0` at the north pole.\n * - motion-script uploads a rasterized surface with `flipY = true`\n * (`skia-render/src/three/handlers/texture.ts`), so the buffer's **first row\n * is `uv.y = 1`**.\n *\n * Solving `(-cos 2πu, sin 2πu)` against {@link latLonToVector}'s `(x, z)` gives\n * `2πu = π + λ`, and the latitude falls straight out of `y = cos θ`. The result\n * is the ordinary world-map layout, which is the point.\n */\nexport function latLonToUV({ lat, lon }: LatLon): { u: number; v: number } {\n return { u: (lon + 180) / 360, v: 0.5 + lat / 180 }\n}\n\n/**\n * Where a latitude and longitude land in the **drawing space** of the baked\n * texture: pixels, **y upward**, origin at the buffer's centre.\n *\n * Two of motion-script's conventions meet here, and neither is guessable from a\n * picture that comes out wrong, so both are written down:\n *\n * - **The origin is the buffer's centre**, because `SkiaRenderContext.rasterize`\n * translates the offscreen canvas by half its size before handing it to the\n * source. Drawn from a top-left origin the map lands a quarter of the world\n * off the edge — and still wraps onto the sphere without complaint.\n * - **`y` is up.** A `PathCommand[]`, and a line's `points`, are authored y-up\n * and mirrored by the backend on the way to Skia (`flipPathY`, and the\n * `points` map beside it in `render-context.ts`). Drawn y-down the world is\n * simply upside down — and every continent still has its own shape, so it\n * reads as a texture flipped somewhere in the pipeline rather than as a sign\n * in the one function that decides.\n *\n * Deliberately *not* the raw buffer's own row order. The backend's flip lands\n * north on the first row, and `flipY` on the uploaded texture maps that row to\n * `uv.y = 1` — which is where {@link latLonToUV} independently puts the north\n * pole. A test ties the two together rather than trusting them to agree.\n */\nexport function latLonToCanvas(\n { lat, lon }: LatLon,\n width: number,\n height: number\n): { x: number; y: number } {\n return { x: (lon / 360) * width, y: (lat / 180) * height }\n}\n\n/**\n * The frame every country path is centred against, in that same space.\n *\n * See {@link WORLD_BOUNDS} for why passing it to each path matters.\n */\nexport function canvasBounds(\n width: number,\n height: number\n): [number, number, number, number] {\n return [-width / 2, -height / 2, width / 2, height / 2]\n}\n\n/**\n * The stored `orbit` heading that puts `lon` at the centre of the frame.\n *\n * **A negation, and it is the price of two things that matter more than the\n * field reading as a longitude.**\n *\n * `orbit` has to behave exactly as it does on every other 3D node: the canvas\n * drag adds to it (`orbitByDrag`), and increasing it must swing the scene the\n * same way it swings a molecule or a surface, or the globe is the one viewport\n * whose drag goes backwards. That pins `orbit` to `cameraOrbit` with no\n * conversion in between.\n *\n * East also has to be on the right, which pins {@link latLonToVector}'s sign.\n *\n * Those two together force the third: a camera at heading θ is centred over\n * longitude −θ. So the stored field is a **heading**, not a longitude — which is\n * why the panel labels it \"Orbit\" like every other viewport's, and why the Fly\n * To command, whose arguments genuinely are a latitude and a longitude,\n * converts through here.\n */\nexport function orbitForLongitude(lon: number): number {\n return -lon\n}\n\n/** The longitude a stored `orbit` heading is centred over. Its own inverse. */\nexport function longitudeForOrbit(orbit: number): number {\n return -orbit\n}\n\n/**\n * Points along the great circle from `from` to `to`, `segments` spans of them.\n *\n * Spherical linear interpolation rather than a lerp of the two lat/lons: the\n * latter is a rhumb line, which crosses the antimeridian the long way and bulges\n * badly at high latitudes — a \"London to Tokyo\" arc drawn that way runs through\n * Kazakhstan instead of over the pole, which is the one thing the picture exists\n * to show.\n *\n * `lift` raises the middle of the arc off the surface, as a fraction of the\n * radius, on a sine so both ends meet the sphere flush. Zero draws the path on\n * the ground.\n */\nexport function greatCircle(\n from: LatLon,\n to: LatLon,\n options: { radius?: number; lift?: number; segments?: number } = {}\n): [number, number, number][] {\n const radius = options.radius ?? 1\n const lift = options.lift ?? 0\n const segments = Math.max(1, Math.floor(options.segments ?? 64))\n\n const a = latLonToVector(from)\n const b = latLonToVector(to)\n\n const dot = clamp(a.x * b.x + a.y * b.y + a.z * b.z, -1, 1)\n const omega = Math.acos(dot)\n\n const points: [number, number, number][] = []\n for (let i = 0; i <= segments; i++) {\n const t = i / segments\n // Antipodal or coincident endpoints leave the plane of the circle\n // undefined; a straight blend is the honest answer for both, and\n // renormalising below puts it back on the sphere either way.\n const [wa, wb] =\n Math.abs(omega) < 1e-6\n ? [1 - t, t]\n : [\n Math.sin((1 - t) * omega) / Math.sin(omega),\n Math.sin(t * omega) / Math.sin(omega),\n ]\n\n const x = a.x * wa + b.x * wb\n const y = a.y * wa + b.y * wb\n const z = a.z * wa + b.z * wb\n const length = Math.hypot(x, y, z) || 1\n const scale = (radius * (1 + lift * Math.sin(Math.PI * t))) / length\n\n points.push([x * scale, y * scale, z * scale])\n }\n return points\n}\n\n/**\n * The signed turn from one heading to another, taking whichever way round is\n * shorter — so 170° to −170° is a 20° nudge east rather than 340° west.\n *\n * The same arithmetic `camera-kit`'s unused `shortestTurn` states; it is\n * duplicated rather than imported because that one is private to a module this\n * package's 3D nodes do not otherwise depend on, and because a Fly To that got\n * this wrong would be the command's single most visible failure.\n */\nexport function shortestTurn(from: number, to: number): number {\n return ((((to - from) % 360) + 540) % 360) - 180\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value\n}\n"]}
@@ -0,0 +1,83 @@
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/core";
49
+ /** Everything that changes the baked pixels. Also the node's cache key. */
50
+ export interface WorldMapStyle {
51
+ /** Buffer width in pixels. Height is half of it — the equirectangular ratio. */
52
+ width: number;
53
+ ocean: string;
54
+ land: string;
55
+ /**
56
+ * The colour of every coast and border.
57
+ *
58
+ * Alpha is fine here, and that is worth saying because it briefly was not:
59
+ * while the country paths were stroked, a shared border was drawn twice and a
60
+ * translucent one came out at double density. `drawBoundaries` draws each line
61
+ * exactly once, so the colour means what it says.
62
+ */
63
+ border: string;
64
+ borderWidth: number;
65
+ graticule: boolean;
66
+ graticuleColor: string;
67
+ /** Degrees between graticule lines. */
68
+ graticuleStep: number;
69
+ graticuleWidth: number;
70
+ }
71
+ /** The buffer's height for a given width — equirectangular is always 2:1. */
72
+ export declare function mapHeight(width: number): number;
73
+ /**
74
+ * The map, drawn.
75
+ *
76
+ * **Hoist the result.** motion-script identifies a surface texture by its source
77
+ * object, so a fresh `Graphics2D` every frame misses the raster cache, redraws
78
+ * ten thousand paths, reads the buffer back off the GPU and uploads it again —
79
+ * sixty times a second, to produce the same image. `Globe` holds one against
80
+ * this style; nothing else should call this.
81
+ */
82
+ export declare function worldMapGraphics(style: WorldMapStyle): Graphics2D;
83
+ //# sourceMappingURL=world-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"world-map.d.ts","sourceRoot":"","sources":["../../src/globe/world-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,EAAE,UAAU,EAAoB,MAAM,oBAAoB,CAAA;AAMjE,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,UAAU,CAuCjE"}