1
2
3
4
5
6
7
8
9
10
11import { MapView, lonLatToMercator } from "mapgpu";
12import { RasterTileLayer, WGSLLayer } from "mapgpu/layers";
13import { RenderEngine } from "mapgpu/render";
14
15import type { RunResultObject } from "@/components/examples/ExampleCanvas";
16
17
18type LocalGPUBuffer = { destroy?: () => void };
19type LocalGPUDevice = {
20 createBuffer(d: { label?: string; size: number; usage: number }): LocalGPUBuffer;
21 queue: { writeBuffer(b: LocalGPUBuffer, offset: number, data: ArrayBufferView): void };
22};
23const VERTEX_USAGE = 0x20;
24const COPY_DST_USAGE = 0x08;
25
26const VERTEX_SHADER = `
27struct CustomUniforms {
28 halfWidth: f32,
29 trailSpeed: f32,
30 trailLength: f32,
31 trailCycle: f32,
32};
33struct VertexOutput {
34 @builtin(position) position: vec4<f32>,
35 @location(0) vColor: vec4<f32>,
36 @location(1) vDistSide: vec2<f32>,
37};
38@vertex
39fn vs_main(
40 @location(0) pos: vec2<f32>,
41 @location(1) offset: vec2<f32>,
42 @location(2) distSide: vec2<f32>,
43 @location(3) color: vec4<f32>,
44) -> VertexOutput {
45 var out: VertexOutput;
46 let clip = projectMercator(pos);
47 if (clip.w < 0.0001) {
48 out.position = vec4<f32>(0,0,0.5,1);
49 out.vColor = vec4<f32>(0);
50 out.vDistSide = vec2<f32>(0, 100);
51 return out;
52 }
53 let clipOff = projectMercator(pos + offset * 50000.0);
54 let sc = clip.xy / clip.w;
55 let so = clipOff.xy / clipOff.w;
56 let dir = normalize((so - sc) * camera.viewport) / camera.viewport * custom.halfWidth * 2.0;
57 out.position = vec4<f32>(clip.xy + dir * clip.w * distSide.y, clip.z, clip.w);
58 out.vColor = color;
59 out.vDistSide = distSide;
60 return out;
61}
62`;
63
64const FRAGMENT_SHADER = `
65@fragment
66fn fs_main(
67 @location(0) vColor: vec4<f32>,
68 @location(1) vDistSide: vec2<f32>,
69) -> @location(0) vec4<f32> {
70 let phase = vDistSide.x - frame.time * custom.trailSpeed;
71 let t = phase - custom.trailCycle * floor(phase / custom.trailCycle);
72 let head = 1.0 - smoothstep(0.0, custom.trailLength, t);
73 let edge = exp(-abs(vDistSide.y) * 3.0);
74 let alpha = min(head * edge * frame.opacity * vColor.a, 0.85);
75 if (alpha < 0.01) { discard; }
76 return vec4<f32>(vColor.rgb * alpha, alpha);
77}
78`;
79
80const STRIDE = 28;
81
82
83const TRACKS: Array<{ path: [number, number][]; color: [number, number, number, number] }> = [
84 {
85 path: buildGreatCircle([29.0, 41.0], [-0.127, 51.507], 40),
86 color: [255, 92, 26, 255],
87 },
88 {
89 path: buildGreatCircle([29.0, 41.0], [139.767, 35.681], 60),
90 color: [67, 176, 255, 255],
91 },
92 {
93 path: buildGreatCircle([29.0, 41.0], [-73.779, 40.642], 50),
94 color: [120, 220, 120, 255],
95 },
96];
97
98function buildGreatCircle(a: [number, number], b: [number, number], steps: number): [number, number][] {
99 const rad = Math.PI / 180;
100 const lon1 = a[0] * rad, lat1 = a[1] * rad;
101 const lon2 = b[0] * rad, lat2 = b[1] * rad;
102 const p1 = [Math.cos(lat1) * Math.cos(lon1), Math.cos(lat1) * Math.sin(lon1), Math.sin(lat1)];
103 const p2 = [Math.cos(lat2) * Math.cos(lon2), Math.cos(lat2) * Math.sin(lon2), Math.sin(lat2)];
104 const d = Math.max(-1, Math.min(1, p1[0] * p2[0] + p1[1] * p2[1] + p1[2] * p2[2]));
105 const omega = Math.acos(d);
106 const sinO = Math.sin(omega) || 1e-6;
107 const out: [number, number][] = [];
108 for (let i = 0; i <= steps; i++) {
109 const t = i / steps;
110 const s1 = Math.sin((1 - t) * omega) / sinO;
111 const s2 = Math.sin(t * omega) / sinO;
112 const x = s1 * p1[0] + s2 * p2[0];
113 const y = s1 * p1[1] + s2 * p2[1];
114 const z = s1 * p1[2] + s2 * p2[2];
115 out.push([Math.atan2(y, x) / rad, Math.asin(z) / rad]);
116 }
117 return out;
118}
119
120
121function buildBuffer(): { data: ArrayBuffer; count: number } {
122 let segCount = 0;
123 for (const t of TRACKS) segCount += t.path.length - 1;
124 const vertCount = segCount * 6;
125 const data = new ArrayBuffer(vertCount * STRIDE);
126 const fv = new Float32Array(data);
127 const bv = new Uint8Array(data);
128
129 let vi = 0;
130 for (const { path, color } of TRACKS) {
131
132 const merc = path.map((c) => lonLatToMercator(c[0], c[1]));
133 const dist: number[] = [0];
134 for (let i = 1; i < merc.length; i++) {
135 const dx = merc[i]![0] - merc[i - 1]![0];
136 const dy = merc[i]![1] - merc[i - 1]![1];
137 dist.push(dist[i - 1]! + Math.hypot(dx, dy));
138 }
139
140 for (let i = 0; i < merc.length - 1; i++) {
141 const a = merc[i]!;
142 const b = merc[i + 1]!;
143 const dx = b[0] - a[0];
144 const dy = b[1] - a[1];
145 const len = Math.hypot(dx, dy) || 1;
146
147 const nx = -dy / len;
148 const ny = dx / len;
149
150 const writeVertex = (pos: [number, number], side: number, d: number) => {
151 const base = vi * STRIDE;
152 fv[base / 4] = pos[0];
153 fv[base / 4 + 1] = pos[1];
154 fv[base / 4 + 2] = nx;
155 fv[base / 4 + 3] = ny;
156 fv[base / 4 + 4] = d;
157 fv[base / 4 + 5] = side;
158 bv[base + 24] = color[0];
159 bv[base + 25] = color[1];
160 bv[base + 26] = color[2];
161 bv[base + 27] = color[3];
162 vi++;
163 };
164 writeVertex(a, -1, dist[i]!);
165 writeVertex(a, 1, dist[i]!);
166 writeVertex(b, -1, dist[i + 1]!);
167 writeVertex(a, 1, dist[i]!);
168 writeVertex(b, 1, dist[i + 1]!);
169 writeVertex(b, -1, dist[i + 1]!);
170 }
171 }
172 return { data, count: vertCount };
173}
174
175export async function run(container: HTMLElement): Promise<RunResultObject> {
176 const view = new MapView({
177 container,
178 renderEngine: new RenderEngine(),
179 mode: "2d",
180 center: [30, 40],
181 zoom: 3,
182 minZoom: 2,
183 maxZoom: 12,
184 backgroundColor: "transparent",
185 });
186
187 view.map.add(
188 new RasterTileLayer({
189 id: "carto-dark",
190 urlTemplate: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
191 subdomains: ["a", "b", "c", "d"],
192 maxZoom: 19,
193 attribution: "© CARTO · © OpenStreetMap contributors",
194 }),
195 );
196
197 const shaderLayer = new WGSLLayer({
198 id: "animated-lines",
199 vertexShader: VERTEX_SHADER,
200 fragmentShader: FRAGMENT_SHADER,
201 vertexBufferLayouts: [
202 {
203 arrayStride: STRIDE,
204 stepMode: "vertex",
205 attributes: [
206 { shaderLocation: 0, offset: 0, format: "float32x2" },
207 { shaderLocation: 1, offset: 8, format: "float32x2" },
208 { shaderLocation: 2, offset: 16, format: "float32x2" },
209 { shaderLocation: 3, offset: 24, format: "unorm8x4" },
210 ],
211 },
212 ],
213 animated: true,
214 topology: "triangle-list",
215 blendState: {
216 color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
217 alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
218 },
219 });
220 view.map.add(shaderLayer);
221
222 await view.when();
223
224
225 shaderLayer.setCustomUniforms(new Float32Array([4, 500000, 200000, 800000]));
226
227 const { data, count } = buildBuffer();
228 const device = view.device as unknown as LocalGPUDevice | null;
229 if (device) {
230 const buf = device.createBuffer({
231 label: "animated-lines-vertex",
232 size: data.byteLength,
233 usage: VERTEX_USAGE | COPY_DST_USAGE,
234 });
235 device.queue.writeBuffer(buf, 0, new Uint8Array(data));
236 shaderLayer.setVertexBuffer(0, buf as unknown as never);
237 shaderLayer.setDrawParams({ vertexCount: count });
238 }
239
240 return {
241 dispose: () => view.destroy(),
242 controls: [],
243 };
244}