roxy-cobewebgl 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/globe.json +43200 -0
- package/globe.min.json +1 -0
- package/index.d.ts +59 -0
- package/package.json +42 -0
- package/src/fallback.js +579 -0
- package/src/globe.worker.js +598 -0
- package/src/index.js +169 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cobe 风格点阵地球 - OffscreenCanvas Worker
|
|
3
|
+
* 整个 WebGL 渲染循环在此 Worker 中运行,不阻塞主线程
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ───────────────────────────────────────────────
|
|
7
|
+
// Vertex Shader
|
|
8
|
+
// ───────────────────────────────────────────────
|
|
9
|
+
const VERTEX_SHADER = `
|
|
10
|
+
attribute vec2 a_position;
|
|
11
|
+
void main() {
|
|
12
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
// ───────────────────────────────────────────────
|
|
17
|
+
// Fragment Shader
|
|
18
|
+
// ───────────────────────────────────────────────
|
|
19
|
+
const FRAGMENT_SHADER = `
|
|
20
|
+
precision highp float;
|
|
21
|
+
|
|
22
|
+
uniform vec2 u_resolution;
|
|
23
|
+
uniform float u_time;
|
|
24
|
+
uniform sampler2D u_texture;
|
|
25
|
+
|
|
26
|
+
uniform float u_phi;
|
|
27
|
+
uniform float u_theta;
|
|
28
|
+
|
|
29
|
+
uniform vec3 u_baseColor;
|
|
30
|
+
uniform vec3 u_glowColor;
|
|
31
|
+
uniform vec3 u_dotColor;
|
|
32
|
+
uniform float u_opacity;
|
|
33
|
+
uniform float u_glowOn;
|
|
34
|
+
uniform float u_debug;
|
|
35
|
+
|
|
36
|
+
uniform float u_dots;
|
|
37
|
+
uniform float u_dotSize;
|
|
38
|
+
uniform float u_globeRadius;
|
|
39
|
+
|
|
40
|
+
const float PI = 3.14159265359;
|
|
41
|
+
const float kPhi = 1.618033988749895;
|
|
42
|
+
const float phiMinusOne = 0.618033988749895;
|
|
43
|
+
const float sqrt5 = 2.23606797749979;
|
|
44
|
+
const float kTau = 6.283185307179586;
|
|
45
|
+
const float twoPiOnPhi = 3.883222077450933;
|
|
46
|
+
const float byLogPhiPlusOne = 1.0 / log2(1.618033988749895 + 1.0);
|
|
47
|
+
|
|
48
|
+
mat3 rotateX(float a) {
|
|
49
|
+
float c = cos(a), s = sin(a);
|
|
50
|
+
return mat3(1., 0., 0.,
|
|
51
|
+
0., c, -s,
|
|
52
|
+
0., s, c);
|
|
53
|
+
}
|
|
54
|
+
mat3 rotateY(float a) {
|
|
55
|
+
float c = cos(a), s = sin(a);
|
|
56
|
+
return mat3( c, 0., s,
|
|
57
|
+
0., 1., 0.,
|
|
58
|
+
-s, 0., c);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
vec3 nearestFibonacciLattice(vec3 p, out float m) {
|
|
62
|
+
float dots = u_dots;
|
|
63
|
+
float byDots = 1.0 / dots;
|
|
64
|
+
p = p.xzy;
|
|
65
|
+
float k = max(2., floor(
|
|
66
|
+
log2(sqrt5 * dots * PI * (1. - p.z * p.z)) * byLogPhiPlusOne
|
|
67
|
+
));
|
|
68
|
+
vec2 f = floor(pow(kPhi, k) / sqrt5 * vec2(1., kPhi) + .5);
|
|
69
|
+
vec2 br1 = fract((f + 1.) * phiMinusOne) * kTau - twoPiOnPhi;
|
|
70
|
+
vec2 br2 = -2. * f;
|
|
71
|
+
vec2 sp = vec2(atan(p.y, p.x), p.z - 1.);
|
|
72
|
+
float denom = br1.x * br2.y - br2.x * br1.y;
|
|
73
|
+
vec2 c = floor(vec2(
|
|
74
|
+
br2.y * sp.x - br1.y * (sp.y * dots + 1.),
|
|
75
|
+
-br2.x * sp.x + br1.x * (sp.y * dots + 1.)
|
|
76
|
+
) / denom);
|
|
77
|
+
float mindist = PI;
|
|
78
|
+
vec3 minip = vec3(0., 0., 1.);
|
|
79
|
+
for (float s = 0.; s < 4.; s += 1.) {
|
|
80
|
+
vec2 o = vec2(mod(s, 2.), floor(s * .5));
|
|
81
|
+
float idx = dot(f, c + o);
|
|
82
|
+
if (idx > dots || idx < 0.) continue;
|
|
83
|
+
float fracV = fract(idx * phiMinusOne);
|
|
84
|
+
float theta = fract(fracV) * kTau;
|
|
85
|
+
float cosphi = 1. - 2. * idx * byDots;
|
|
86
|
+
float sinphi = sqrt(1. - cosphi * cosphi);
|
|
87
|
+
vec3 sample2 = vec3(cos(theta) * sinphi, sin(theta) * sinphi, cosphi);
|
|
88
|
+
float dist = length(p - sample2);
|
|
89
|
+
if (dist < mindist) {
|
|
90
|
+
mindist = dist;
|
|
91
|
+
minip = sample2;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
m = mindist;
|
|
95
|
+
return minip.xzy;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
void main() {
|
|
99
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
100
|
+
float aspect = u_resolution.x / u_resolution.y;
|
|
101
|
+
vec2 p = uv * 2. - 1.;
|
|
102
|
+
p.x *= aspect;
|
|
103
|
+
|
|
104
|
+
float r = u_globeRadius;
|
|
105
|
+
float d2 = dot(p, p);
|
|
106
|
+
float rSquared = r * r;
|
|
107
|
+
float l = d2;
|
|
108
|
+
|
|
109
|
+
float glowFactor = 0.;
|
|
110
|
+
vec4 color = vec4(0.);
|
|
111
|
+
|
|
112
|
+
if (d2 < rSquared) {
|
|
113
|
+
float z = sqrt(rSquared - d2);
|
|
114
|
+
vec3 nor = normalize(vec3(p.x, p.y, z));
|
|
115
|
+
nor = rotateY(u_phi) * rotateX(u_theta) * nor;
|
|
116
|
+
float dis;
|
|
117
|
+
vec3 gP = nearestFibonacciLattice(nor, dis);
|
|
118
|
+
float gLat = asin(gP.y);
|
|
119
|
+
float gLng = PI * 0.5 - atan(gP.z, gP.x);
|
|
120
|
+
vec2 dotUV = vec2(fract(gLng / kTau + 0.5), gLat / PI + 0.5);
|
|
121
|
+
float texSample = texture2D(u_texture, dotUV).r;
|
|
122
|
+
float mapColor = step(0.5, texSample);
|
|
123
|
+
float v = step(dis, u_dotSize);
|
|
124
|
+
|
|
125
|
+
if (u_debug > 0.5 && u_debug < 1.5) {
|
|
126
|
+
float dotMask = v;
|
|
127
|
+
vec3 surfaceColor = mix(u_baseColor, u_dotColor, dotMask);
|
|
128
|
+
color = vec4(surfaceColor * u_opacity, 1.);
|
|
129
|
+
} else if (u_debug > 1.5 && u_debug < 2.5) {
|
|
130
|
+
color = vec4(dotUV.x, dotUV.y, 0.0, 1.0);
|
|
131
|
+
} else if (u_debug > 2.5 && u_debug < 3.5) {
|
|
132
|
+
float dotMask = v * mapColor;
|
|
133
|
+
vec3 surfaceColor = mix(vec3(texSample * 0.3), u_dotColor, dotMask);
|
|
134
|
+
color = vec4(surfaceColor, 1.);
|
|
135
|
+
} else {
|
|
136
|
+
float dotMask = mapColor * v;
|
|
137
|
+
vec3 surfaceColor = mix(u_baseColor, u_dotColor, dotMask);
|
|
138
|
+
color = vec4(surfaceColor * u_opacity, 1.);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
float edgeFade = sqrt(1.0 - d2 / rSquared);
|
|
142
|
+
color.rgb *= mix(0.3, 1.0, edgeFade);
|
|
143
|
+
|
|
144
|
+
glowFactor = u_glowOn * pow(
|
|
145
|
+
dot(normalize(vec3(-uv, sqrt(1. - l))), vec3(0., 0., 1.)),
|
|
146
|
+
4.
|
|
147
|
+
) * smoothstep(0., 1., .2 / (l - rSquared));
|
|
148
|
+
|
|
149
|
+
} else {
|
|
150
|
+
float outD = sqrt(0.2 / (l - rSquared));
|
|
151
|
+
glowFactor = u_glowOn * smoothstep(0.5, 1., outD / (outD + 1.));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
gl_FragColor = color + vec4(glowFactor * u_glowColor, glowFactor);
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
// ───────────────────────────────────────────────
|
|
159
|
+
// Arc Vertex Shader
|
|
160
|
+
// ───────────────────────────────────────────────
|
|
161
|
+
const ARC_VERTEX_SHADER = `
|
|
162
|
+
attribute vec3 a_arcPos;
|
|
163
|
+
uniform float u_phi;
|
|
164
|
+
uniform float u_theta;
|
|
165
|
+
uniform float u_aspect;
|
|
166
|
+
uniform float u_globeRadius;
|
|
167
|
+
varying float v_z;
|
|
168
|
+
|
|
169
|
+
mat3 rotateX(float a) {
|
|
170
|
+
float c = cos(a), s = sin(a);
|
|
171
|
+
return mat3(1., 0., 0., 0., c, -s, 0., s, c);
|
|
172
|
+
}
|
|
173
|
+
mat3 rotateY(float a) {
|
|
174
|
+
float c = cos(a), s = sin(a);
|
|
175
|
+
return mat3(c, 0., s, 0., 1., 0., -s, 0., c);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
void main() {
|
|
179
|
+
vec3 viewPos = rotateX(-u_theta) * rotateY(-u_phi) * a_arcPos;
|
|
180
|
+
v_z = viewPos.z;
|
|
181
|
+
gl_Position = vec4(viewPos.x * u_globeRadius / u_aspect, viewPos.y * u_globeRadius, 0.0, 1.0);
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
// ───────────────────────────────────────────────
|
|
186
|
+
// Arc Fragment Shader
|
|
187
|
+
// ───────────────────────────────────────────────
|
|
188
|
+
const ARC_FRAGMENT_SHADER = `
|
|
189
|
+
precision highp float;
|
|
190
|
+
uniform vec3 u_arcColor;
|
|
191
|
+
uniform float u_arcAlpha;
|
|
192
|
+
uniform float u_globeRadius;
|
|
193
|
+
uniform vec2 u_resolution;
|
|
194
|
+
varying float v_z;
|
|
195
|
+
|
|
196
|
+
void main() {
|
|
197
|
+
vec2 ndc = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
|
|
198
|
+
float aspect = u_resolution.x / u_resolution.y;
|
|
199
|
+
vec2 p = vec2(ndc.x * aspect, ndc.y);
|
|
200
|
+
float d2 = dot(p, p);
|
|
201
|
+
float rSq = u_globeRadius * u_globeRadius;
|
|
202
|
+
|
|
203
|
+
if (d2 < rSq) {
|
|
204
|
+
float sphereFrontZ = sqrt(1.0 - d2 / rSq);
|
|
205
|
+
if (v_z < sphereFrontZ) discard;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
gl_FragColor = vec4(u_arcColor, u_arcAlpha);
|
|
209
|
+
}
|
|
210
|
+
`;
|
|
211
|
+
|
|
212
|
+
// ───────────────────────────────────────────────
|
|
213
|
+
// 工具函数
|
|
214
|
+
// ───────────────────────────────────────────────
|
|
215
|
+
function createProgram(gl, vsSrc, fsSrc) {
|
|
216
|
+
const vs = gl.createShader(gl.VERTEX_SHADER);
|
|
217
|
+
gl.shaderSource(vs, vsSrc);
|
|
218
|
+
gl.compileShader(vs);
|
|
219
|
+
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
|
|
220
|
+
console.error('Vertex shader error:', gl.getShaderInfoLog(vs));
|
|
221
|
+
gl.deleteShader(vs);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
|
225
|
+
gl.shaderSource(fs, fsSrc);
|
|
226
|
+
gl.compileShader(fs);
|
|
227
|
+
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
|
|
228
|
+
console.error('Fragment shader error:', gl.getShaderInfoLog(fs));
|
|
229
|
+
gl.deleteShader(fs);
|
|
230
|
+
gl.deleteShader(vs);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const prog = gl.createProgram();
|
|
234
|
+
gl.attachShader(prog, vs);
|
|
235
|
+
gl.attachShader(prog, fs);
|
|
236
|
+
gl.linkProgram(prog);
|
|
237
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
238
|
+
console.error('Link error:', gl.getProgramInfoLog(prog));
|
|
239
|
+
gl.deleteProgram(prog);
|
|
240
|
+
gl.deleteShader(vs);
|
|
241
|
+
gl.deleteShader(fs);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
gl.deleteShader(vs);
|
|
245
|
+
gl.deleteShader(fs);
|
|
246
|
+
return prog;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ───────────────────────────────────────────────
|
|
250
|
+
// 纹理生成(Worker 中用 OffscreenCanvas 2D 画世界地图)
|
|
251
|
+
// ───────────────────────────────────────────────
|
|
252
|
+
function createWorldTexture(gl, geojson) {
|
|
253
|
+
const W = 720, H = 360;
|
|
254
|
+
const offCanvas = new OffscreenCanvas(W, H);
|
|
255
|
+
const ctx = offCanvas.getContext('2d');
|
|
256
|
+
|
|
257
|
+
ctx.fillStyle = '#000';
|
|
258
|
+
ctx.fillRect(0, 0, W, H);
|
|
259
|
+
ctx.fillStyle = '#fff';
|
|
260
|
+
|
|
261
|
+
for (const feature of geojson.features) {
|
|
262
|
+
const geom = feature.geometry;
|
|
263
|
+
if (!geom) continue;
|
|
264
|
+
let polys = [];
|
|
265
|
+
if (geom.type === 'Polygon') polys = [geom.coordinates];
|
|
266
|
+
else if (geom.type === 'MultiPolygon') polys = geom.coordinates;
|
|
267
|
+
|
|
268
|
+
for (const poly of polys) {
|
|
269
|
+
ctx.beginPath();
|
|
270
|
+
const ring = poly[0];
|
|
271
|
+
for (let i = 0; i < ring.length; i++) {
|
|
272
|
+
const [lng, lat] = ring[i];
|
|
273
|
+
const x = (lng + 180) / 360 * W;
|
|
274
|
+
const y = (90 - lat) / 180 * H;
|
|
275
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
276
|
+
else ctx.lineTo(x, y);
|
|
277
|
+
}
|
|
278
|
+
ctx.closePath();
|
|
279
|
+
ctx.fill();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const imgData = ctx.getImageData(0, 0, W, H);
|
|
284
|
+
|
|
285
|
+
// Worker 中手动翻转 Y(UNPACK_FLIP_Y_WEBGL 对 ArrayBufferView 不生效)
|
|
286
|
+
const pixels = new Uint8Array(imgData.data);
|
|
287
|
+
const rowBytes = W * 4;
|
|
288
|
+
const tmp = new Uint8Array(rowBytes);
|
|
289
|
+
for (let y = 0; y < H / 2; y++) {
|
|
290
|
+
const top = y * rowBytes, bot = (H - 1 - y) * rowBytes;
|
|
291
|
+
tmp.set(pixels.subarray(top, top + rowBytes));
|
|
292
|
+
pixels.copyWithin(top, bot, bot + rowBytes);
|
|
293
|
+
pixels.set(tmp, bot);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const tex = gl.createTexture();
|
|
297
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
298
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, W, H, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
299
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
300
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
301
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
302
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
303
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
304
|
+
return tex;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ───────────────────────────────────────────────
|
|
308
|
+
// 弧线工具
|
|
309
|
+
// ───────────────────────────────────────────────
|
|
310
|
+
const DEG2RAD = Math.PI / 180;
|
|
311
|
+
|
|
312
|
+
function latLngToVec3(lat, lng) {
|
|
313
|
+
const latR = lat * DEG2RAD, lngR = lng * DEG2RAD;
|
|
314
|
+
return [Math.cos(latR) * Math.sin(lngR), Math.sin(latR), Math.cos(latR) * Math.cos(lngR)];
|
|
315
|
+
}
|
|
316
|
+
function vec3Dot(a, b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }
|
|
317
|
+
function vec3Length(a) { return Math.sqrt(vec3Dot(a, a)); }
|
|
318
|
+
function vec3Normalize(a) {
|
|
319
|
+
const l = vec3Length(a);
|
|
320
|
+
return l > 1e-8 ? [a[0]/l, a[1]/l, a[2]/l] : [0, 0, 1];
|
|
321
|
+
}
|
|
322
|
+
function slerp(a, b, t) {
|
|
323
|
+
const d = Math.max(-1, Math.min(1, vec3Dot(a, b)));
|
|
324
|
+
const angle = Math.acos(d);
|
|
325
|
+
if (angle < 0.001) {
|
|
326
|
+
return vec3Normalize([a[0]*(1-t)+b[0]*t, a[1]*(1-t)+b[1]*t, a[2]*(1-t)+b[2]*t]);
|
|
327
|
+
}
|
|
328
|
+
const sinA = Math.sin(angle);
|
|
329
|
+
const wa = Math.sin((1-t)*angle)/sinA, wb = Math.sin(t*angle)/sinA;
|
|
330
|
+
return [a[0]*wa+b[0]*wb, a[1]*wa+b[1]*wb, a[2]*wa+b[2]*wb];
|
|
331
|
+
}
|
|
332
|
+
function generateArcPoints(startVec, endVec, numSegments) {
|
|
333
|
+
numSegments = numSegments || 64;
|
|
334
|
+
const d = Math.max(-1, Math.min(1, vec3Dot(startVec, endVec)));
|
|
335
|
+
const angularDist = Math.acos(d);
|
|
336
|
+
const arcHeight = Math.min(0.4, 0.1 + 0.2 * angularDist);
|
|
337
|
+
const points = [];
|
|
338
|
+
for (let i = 0; i <= numSegments; i++) {
|
|
339
|
+
const t = i / numSegments;
|
|
340
|
+
const p = slerp(startVec, endVec, t);
|
|
341
|
+
const elevation = 1.0 + arcHeight * Math.sin(Math.PI * t);
|
|
342
|
+
points.push(p[0]*elevation, p[1]*elevation, p[2]*elevation);
|
|
343
|
+
}
|
|
344
|
+
return new Float32Array(points);
|
|
345
|
+
}
|
|
346
|
+
function parseHexColor(hex) {
|
|
347
|
+
const n = parseInt(hex.replace('#', ''), 16);
|
|
348
|
+
return [(n>>16&255)/255, (n>>8&255)/255, (n&255)/255];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ───────────────────────────────────────────────
|
|
352
|
+
// 弧线动画管理器
|
|
353
|
+
// ───────────────────────────────────────────────
|
|
354
|
+
class ArcAnimator {
|
|
355
|
+
constructor(arcsData, gl) {
|
|
356
|
+
this.gl = gl;
|
|
357
|
+
this.arcs = arcsData.slice().sort((a, b) => a.order - b.order).map(arc => {
|
|
358
|
+
const startVec = latLngToVec3(arc.startLat, arc.startLng);
|
|
359
|
+
const endVec = latLngToVec3(arc.endLat, arc.endLng);
|
|
360
|
+
const points = generateArcPoints(startVec, endVec, 64);
|
|
361
|
+
const buffer = gl.createBuffer();
|
|
362
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
363
|
+
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
|
|
364
|
+
const numPoints = points.length / 3;
|
|
365
|
+
const d = Math.max(-1, Math.min(1, vec3Dot(startVec, endVec)));
|
|
366
|
+
const angularDist = Math.acos(d);
|
|
367
|
+
const color = arc.color ? parseHexColor(arc.color) : null;
|
|
368
|
+
const tailLen = Math.max(5, Math.floor(numPoints * 0.3));
|
|
369
|
+
const headEnd = numPoints - 1;
|
|
370
|
+
const totalDuration = 1.2 + 1.5 * angularDist;
|
|
371
|
+
const travelRatio = headEnd / (headEnd + tailLen);
|
|
372
|
+
const travelDuration = totalDuration * travelRatio;
|
|
373
|
+
const drainDuration = totalDuration * (1 - travelRatio);
|
|
374
|
+
return { buffer, numPoints, color, travelDuration, drainDuration, totalDuration, tailLen, headEnd };
|
|
375
|
+
});
|
|
376
|
+
this.arcDelay = 1.0;
|
|
377
|
+
const maxDur = Math.max(...this.arcs.map(a => a.totalDuration));
|
|
378
|
+
this.cycleDuration = Math.max(1, this.arcs.length - 1) * this.arcDelay + maxDur + 1.0;
|
|
379
|
+
}
|
|
380
|
+
getState(arcIndex, timeSec) {
|
|
381
|
+
const arc = this.arcs[arcIndex];
|
|
382
|
+
const cycleTime = timeSec % this.cycleDuration;
|
|
383
|
+
const arcStart = arcIndex * this.arcDelay;
|
|
384
|
+
const local = cycleTime - arcStart;
|
|
385
|
+
if (local < 0 || local > arc.totalDuration) return null;
|
|
386
|
+
const tailLen = arc.tailLen, headEnd = arc.headEnd;
|
|
387
|
+
if (local < arc.travelDuration) {
|
|
388
|
+
const p = local / arc.travelDuration;
|
|
389
|
+
const head = Math.min(headEnd, Math.floor(p * (headEnd + 0.9999)));
|
|
390
|
+
const tail = Math.max(0, head - tailLen);
|
|
391
|
+
return { startIdx: tail, drawCount: Math.max(2, head - tail + 1), alpha: 1.0 };
|
|
392
|
+
}
|
|
393
|
+
const drainP = (local - arc.travelDuration) / arc.drainDuration;
|
|
394
|
+
const tail = Math.floor((headEnd - tailLen) + tailLen * drainP);
|
|
395
|
+
const count = headEnd - tail + 1;
|
|
396
|
+
if (count < 2) return null;
|
|
397
|
+
return { startIdx: Math.min(tail, headEnd - 1), drawCount: Math.max(2, count), alpha: 1.0 };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ───────────────────────────────────────────────
|
|
402
|
+
// Worker 主逻辑
|
|
403
|
+
// ───────────────────────────────────────────────
|
|
404
|
+
let gl, canvas;
|
|
405
|
+
let program, loc, arcProgram, arcLoc;
|
|
406
|
+
let quadBuffer, worldTex, arcAnimator;
|
|
407
|
+
|
|
408
|
+
let running = false;
|
|
409
|
+
let rafId = 0;
|
|
410
|
+
|
|
411
|
+
function normalizeArcsData(arcsData) {
|
|
412
|
+
return (arcsData || []).map((a, i) => ({
|
|
413
|
+
order: a.order != null ? a.order : i + 1,
|
|
414
|
+
startLat: a.startLat,
|
|
415
|
+
startLng: a.startLng,
|
|
416
|
+
endLat: a.endLat,
|
|
417
|
+
endLng: a.endLng,
|
|
418
|
+
color: a.color,
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 可由主线程实时更新的参数
|
|
423
|
+
let params = {
|
|
424
|
+
phi: 0.3, theta: 0.15,
|
|
425
|
+
baseColor: [0.06, 0.1, 0.2],
|
|
426
|
+
glowColor: [0.15, 0.4, 0.85],
|
|
427
|
+
dotColor: [0.4, 0.8, 1.0],
|
|
428
|
+
arcColor: [1.0, 0.4, 0.2],
|
|
429
|
+
dots: 800, dotSize: 0.008,
|
|
430
|
+
globeRadius: 0.55,
|
|
431
|
+
glowOn: 1.0,
|
|
432
|
+
debug: 0.0,
|
|
433
|
+
opacity: 0.9,
|
|
434
|
+
autoRotate: true,
|
|
435
|
+
rotationSpeed: 0.003,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
async function init(msg) {
|
|
439
|
+
try {
|
|
440
|
+
canvas = msg.canvas;
|
|
441
|
+
if (msg.params) Object.assign(params, msg.params);
|
|
442
|
+
|
|
443
|
+
console.log('[Worker] init, canvas size:', canvas.width, canvas.height);
|
|
444
|
+
gl = canvas.getContext('webgl', { antialias: false, alpha: false });
|
|
445
|
+
if (!gl) {
|
|
446
|
+
self.postMessage({ type: 'error', msg: 'WebGL 不可用' });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
console.log('[Worker] WebGL context OK');
|
|
450
|
+
|
|
451
|
+
program = createProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER);
|
|
452
|
+
if (!program) {
|
|
453
|
+
self.postMessage({ type: 'error', msg: '主着色器编译失败' });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
console.log('[Worker] Main shader OK');
|
|
457
|
+
|
|
458
|
+
loc = {};
|
|
459
|
+
loc.a_position = gl.getAttribLocation(program, 'a_position');
|
|
460
|
+
['u_resolution','u_time','u_texture','u_phi','u_theta','u_baseColor','u_glowColor','u_dotColor','u_opacity','u_glowOn','u_dots','u_dotSize','u_globeRadius','u_debug'].forEach(
|
|
461
|
+
n => loc[n] = gl.getUniformLocation(program, n)
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
quadBuffer = gl.createBuffer();
|
|
465
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
466
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]), gl.STATIC_DRAW);
|
|
467
|
+
|
|
468
|
+
let geojson;
|
|
469
|
+
if (msg.map && typeof msg.map === 'object') {
|
|
470
|
+
geojson = msg.map;
|
|
471
|
+
console.log('[Worker] GeoJSON from message, features:', geojson.features && geojson.features.length);
|
|
472
|
+
} else if (msg.mapUrl) {
|
|
473
|
+
console.log('[Worker] Fetching map …', String(msg.mapUrl).slice(0, 80));
|
|
474
|
+
const resp = await fetch(String(msg.mapUrl));
|
|
475
|
+
if (!resp.ok) {
|
|
476
|
+
self.postMessage({ type: 'error', msg: 'fetch map failed: ' + resp.status });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
geojson = await resp.json();
|
|
480
|
+
console.log('[Worker] GeoJSON parsed, features:', geojson.features.length);
|
|
481
|
+
} else {
|
|
482
|
+
self.postMessage({ type: 'error', msg: 'init requires map or mapUrl' });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
worldTex = createWorldTexture(gl, geojson);
|
|
487
|
+
console.log('[Worker] Texture created');
|
|
488
|
+
|
|
489
|
+
const arcsData = normalizeArcsData(msg.arcsData);
|
|
490
|
+
arcProgram = null;
|
|
491
|
+
arcAnimator = null;
|
|
492
|
+
if (arcsData.length > 0) {
|
|
493
|
+
arcProgram = createProgram(gl, ARC_VERTEX_SHADER, ARC_FRAGMENT_SHADER);
|
|
494
|
+
arcLoc = {};
|
|
495
|
+
if (arcProgram) {
|
|
496
|
+
arcLoc.a_arcPos = gl.getAttribLocation(arcProgram, 'a_arcPos');
|
|
497
|
+
['u_phi','u_theta','u_aspect','u_globeRadius','u_arcColor','u_arcAlpha','u_resolution'].forEach(
|
|
498
|
+
n => arcLoc[n] = gl.getUniformLocation(arcProgram, n)
|
|
499
|
+
);
|
|
500
|
+
arcAnimator = new ArcAnimator(arcsData, gl);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
console.log('[Worker] Init complete, starting render loop');
|
|
504
|
+
|
|
505
|
+
running = true;
|
|
506
|
+
rafId = requestAnimationFrame(render);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
console.error('[Worker] init error:', err);
|
|
509
|
+
self.postMessage({ type: 'error', msg: String(err) });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function resize() {
|
|
514
|
+
const w = canvas.width, h = canvas.height;
|
|
515
|
+
gl.viewport(0, 0, w, h);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function render(t) {
|
|
519
|
+
if (!running) return;
|
|
520
|
+
resize();
|
|
521
|
+
const timeSec = t * 0.001;
|
|
522
|
+
if (params.autoRotate) params.phi += params.rotationSpeed || 0.003;
|
|
523
|
+
|
|
524
|
+
// Pass 1: 球体
|
|
525
|
+
gl.useProgram(program);
|
|
526
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
527
|
+
gl.enableVertexAttribArray(loc.a_position);
|
|
528
|
+
gl.vertexAttribPointer(loc.a_position, 2, gl.FLOAT, false, 0, 0);
|
|
529
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
530
|
+
gl.bindTexture(gl.TEXTURE_2D, worldTex);
|
|
531
|
+
|
|
532
|
+
gl.uniform2f(loc.u_resolution, canvas.width, canvas.height);
|
|
533
|
+
gl.uniform1f(loc.u_time, timeSec);
|
|
534
|
+
gl.uniform1i(loc.u_texture, 0);
|
|
535
|
+
gl.uniform1f(loc.u_phi, params.phi);
|
|
536
|
+
gl.uniform1f(loc.u_theta, params.theta);
|
|
537
|
+
gl.uniform3fv(loc.u_baseColor, params.baseColor);
|
|
538
|
+
gl.uniform3fv(loc.u_glowColor, params.glowColor);
|
|
539
|
+
gl.uniform3fv(loc.u_dotColor, params.dotColor);
|
|
540
|
+
gl.uniform1f(loc.u_opacity, params.opacity != null ? params.opacity : 0.9);
|
|
541
|
+
gl.uniform1f(loc.u_glowOn, params.glowOn);
|
|
542
|
+
gl.uniform1f(loc.u_dots, params.dots);
|
|
543
|
+
gl.uniform1f(loc.u_dotSize, params.dotSize);
|
|
544
|
+
gl.uniform1f(loc.u_globeRadius, params.globeRadius);
|
|
545
|
+
gl.uniform1f(loc.u_debug, params.debug);
|
|
546
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
547
|
+
|
|
548
|
+
// Pass 2: 弧线
|
|
549
|
+
if (arcProgram && arcAnimator) {
|
|
550
|
+
gl.enable(gl.BLEND);
|
|
551
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
552
|
+
gl.useProgram(arcProgram);
|
|
553
|
+
const aspect = canvas.width / canvas.height;
|
|
554
|
+
gl.uniform1f(arcLoc.u_phi, params.phi);
|
|
555
|
+
gl.uniform1f(arcLoc.u_theta, params.theta);
|
|
556
|
+
gl.uniform1f(arcLoc.u_aspect, aspect);
|
|
557
|
+
gl.uniform1f(arcLoc.u_globeRadius, params.globeRadius);
|
|
558
|
+
gl.uniform2f(arcLoc.u_resolution, canvas.width, canvas.height);
|
|
559
|
+
|
|
560
|
+
try { gl.lineWidth(2.0); } catch (e) { /* ignore */ }
|
|
561
|
+
|
|
562
|
+
for (let i = 0; i < arcAnimator.arcs.length; i++) {
|
|
563
|
+
const state = arcAnimator.getState(i, timeSec);
|
|
564
|
+
if (!state) continue;
|
|
565
|
+
const color = arcAnimator.arcs[i].color || params.arcColor;
|
|
566
|
+
gl.uniform3fv(arcLoc.u_arcColor, color);
|
|
567
|
+
gl.uniform1f(arcLoc.u_arcAlpha, state.alpha);
|
|
568
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, arcAnimator.arcs[i].buffer);
|
|
569
|
+
gl.enableVertexAttribArray(arcLoc.a_arcPos);
|
|
570
|
+
gl.vertexAttribPointer(arcLoc.a_arcPos, 3, gl.FLOAT, false, 0, 0);
|
|
571
|
+
gl.drawArrays(gl.LINE_STRIP, state.startIdx, state.drawCount);
|
|
572
|
+
}
|
|
573
|
+
gl.disable(gl.BLEND);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
rafId = requestAnimationFrame(render);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ───────────────────────────────────────────────
|
|
580
|
+
// 消息处理
|
|
581
|
+
// ───────────────────────────────────────────────
|
|
582
|
+
self.onmessage = function(e) {
|
|
583
|
+
const msg = e.data;
|
|
584
|
+
if (msg.type === 'init') {
|
|
585
|
+
init(msg);
|
|
586
|
+
} else if (msg.type === 'resize') {
|
|
587
|
+
if (canvas) {
|
|
588
|
+
canvas.width = msg.width;
|
|
589
|
+
canvas.height = msg.height;
|
|
590
|
+
}
|
|
591
|
+
} else if (msg.type === 'params') {
|
|
592
|
+
Object.assign(params, msg.params);
|
|
593
|
+
} else if (msg.type === 'destroy') {
|
|
594
|
+
running = false;
|
|
595
|
+
cancelAnimationFrame(rafId);
|
|
596
|
+
rafId = 0;
|
|
597
|
+
}
|
|
598
|
+
};
|