canvas-globe 1.1.1 → 1.2.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/CHANGELOG.md +24 -0
- package/README.md +76 -4
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/codemeta.json +1 -1
- package/dist/canvas-globe.umd.js +171 -5
- package/package.json +34 -2
- package/src/charts.js +458 -0
- package/src/controls.js +187 -0
- package/src/data/places.js +5 -0
- package/src/fx/effects/ambient.js +355 -0
- package/src/fx/effects/data.js +438 -0
- package/src/fx/effects/entrances.js +111 -0
- package/src/fx/effects/interaction.js +850 -0
- package/src/fx/effects/scene.js +571 -0
- package/src/fx/effects/transitions.js +209 -0
- package/src/fx/index.js +52 -0
- package/src/fx/pointer.js +94 -0
- package/src/fx/runtime.js +161 -0
- package/src/geo-globe.js +170 -4
- package/src/places.js +97 -0
- package/src/recipes.js +188 -0
- package/src/version.js +1 -1
- package/types/charts.d.ts +82 -0
- package/types/controls.d.ts +54 -0
- package/types/fx-interaction.d.ts +16 -0
- package/types/fx.d.ts +313 -0
- package/types/index.d.ts +48 -0
- package/types/places.d.ts +43 -0
- package/types/recipes.d.ts +54 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/** Data reveals and attention grabbers. */
|
|
2
|
+
import { clamp01, drawPath, easeOut, lerp, readout, ring, stagger, TAU } from "../runtime.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An odometer headline easing to its final number.
|
|
6
|
+
* Ports catalogue effect O.
|
|
7
|
+
*/
|
|
8
|
+
export const counterRoll = ({
|
|
9
|
+
to = 0,
|
|
10
|
+
from = 0,
|
|
11
|
+
duration = 2600,
|
|
12
|
+
hold = 1400,
|
|
13
|
+
caption = "",
|
|
14
|
+
format = (n) => Math.round(n).toLocaleString(),
|
|
15
|
+
size = 34,
|
|
16
|
+
color = "#fff",
|
|
17
|
+
position = "bottom-left",
|
|
18
|
+
} = {}) => ({
|
|
19
|
+
name: "counterRoll",
|
|
20
|
+
stage: "above",
|
|
21
|
+
duration,
|
|
22
|
+
hold,
|
|
23
|
+
frame(ctx, globe, t) {
|
|
24
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
25
|
+
const value = from + (to - from) * easeOut(t);
|
|
26
|
+
const pad = 16;
|
|
27
|
+
ctx.font = `800 ${size}px Inter, system-ui, sans-serif`;
|
|
28
|
+
const text = format(value);
|
|
29
|
+
const tw = ctx.measureText(text).width;
|
|
30
|
+
const centred = position.includes("center");
|
|
31
|
+
const x = centred ? (w - tw) / 2 : position.includes("right") ? w - pad - tw : pad;
|
|
32
|
+
const y = position.includes("top") ? pad + size : h - pad - (caption ? 16 : 0);
|
|
33
|
+
ctx.fillStyle = color;
|
|
34
|
+
ctx.shadowColor = "rgba(0,0,0,.4)";
|
|
35
|
+
ctx.shadowBlur = 10;
|
|
36
|
+
ctx.fillText(text, x, y);
|
|
37
|
+
if (caption) {
|
|
38
|
+
ctx.shadowBlur = 0;
|
|
39
|
+
ctx.font = `600 ${Math.round(size * 0.3)}px Inter, system-ui, sans-serif`;
|
|
40
|
+
ctx.fillStyle = "rgba(232,236,245,.72)";
|
|
41
|
+
const cw = ctx.measureText(caption).width;
|
|
42
|
+
ctx.fillText(caption, centred ? (w - cw) / 2 : x, y + size * 0.34);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A true great-circle ring expanding across the curved surface.
|
|
49
|
+
* Ports catalogue effect Q.
|
|
50
|
+
*/
|
|
51
|
+
export const shockwave = ({
|
|
52
|
+
at,
|
|
53
|
+
duration = 2600,
|
|
54
|
+
hold = 400,
|
|
55
|
+
color = "167,139,250",
|
|
56
|
+
reach = 165,
|
|
57
|
+
rings = 3,
|
|
58
|
+
} = {}) => ({
|
|
59
|
+
name: "shockwave",
|
|
60
|
+
stage: "above",
|
|
61
|
+
duration,
|
|
62
|
+
hold,
|
|
63
|
+
frame(ctx, globe, t) {
|
|
64
|
+
const origin = at || globe.markers[0];
|
|
65
|
+
if (!origin) return;
|
|
66
|
+
ctx.lineWidth = 2.2;
|
|
67
|
+
for (let i = 0; i < rings; i++) {
|
|
68
|
+
const p = t - i * 0.18;
|
|
69
|
+
if (p <= 0 || p >= 1) continue;
|
|
70
|
+
ctx.strokeStyle = `rgba(${color},${(1 - i * 0.35) * (1 - p)})`;
|
|
71
|
+
ctx.beginPath();
|
|
72
|
+
drawPath(ctx, globe, ring(origin.lon, origin.lat, p * reach, 90));
|
|
73
|
+
ctx.stroke();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Countries flood with colour in ranked order.
|
|
80
|
+
* Ports catalogue effect M.
|
|
81
|
+
*/
|
|
82
|
+
export const choroplethCascade = ({
|
|
83
|
+
values = {},
|
|
84
|
+
duration = 3200,
|
|
85
|
+
hold = 1000,
|
|
86
|
+
hue = 258,
|
|
87
|
+
} = {}) => ({
|
|
88
|
+
name: "choroplethCascade",
|
|
89
|
+
stage: "above",
|
|
90
|
+
duration,
|
|
91
|
+
hold,
|
|
92
|
+
setup(globe) {
|
|
93
|
+
const ranked = Object.entries(values).sort((a, b) => b[1] - a[1]);
|
|
94
|
+
const max = Math.max(1, ...ranked.map((r) => r[1]));
|
|
95
|
+
const shapes = ranked
|
|
96
|
+
.map(([key, value]) => ({ shape: globe._countryShape(key), value }))
|
|
97
|
+
.filter((r) => r.shape);
|
|
98
|
+
return { shapes, max };
|
|
99
|
+
},
|
|
100
|
+
frame(ctx, globe, t, { shapes, max }) {
|
|
101
|
+
shapes.forEach(({ shape, value }, i) => {
|
|
102
|
+
const k = stagger(t, i, shapes.length, 0.3);
|
|
103
|
+
if (k <= 0) return;
|
|
104
|
+
ctx.beginPath();
|
|
105
|
+
globe.tracePath(shape, ctx);
|
|
106
|
+
ctx.fillStyle = `hsl(${hue} 85% ${72 - (value / max) * 40}% / ${clamp01(k)})`;
|
|
107
|
+
ctx.fill();
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Routes draw themselves as dashes with a glyph at the head.
|
|
114
|
+
* Ports catalogue effect AA.
|
|
115
|
+
*/
|
|
116
|
+
export const routeDashes = ({
|
|
117
|
+
routes = [],
|
|
118
|
+
duration = 4200,
|
|
119
|
+
hold = 1200,
|
|
120
|
+
color = "#ef4444",
|
|
121
|
+
icon = "✈️",
|
|
122
|
+
steps = 64,
|
|
123
|
+
} = {}) => ({
|
|
124
|
+
name: "routeDashes",
|
|
125
|
+
stage: "above",
|
|
126
|
+
duration,
|
|
127
|
+
hold,
|
|
128
|
+
setup() {
|
|
129
|
+
return { paths: routes.map((r) => ({ from: r.from, to: r.to, points: null })) };
|
|
130
|
+
},
|
|
131
|
+
frame(ctx, globe, t, state) {
|
|
132
|
+
state.paths.forEach((leg, i) => {
|
|
133
|
+
if (!leg.points) {
|
|
134
|
+
const pts = [];
|
|
135
|
+
for (let s = 0; s <= steps; s++) {
|
|
136
|
+
const f = s / steps;
|
|
137
|
+
pts.push([
|
|
138
|
+
leg.from.lon + (leg.to.lon - leg.from.lon) * f,
|
|
139
|
+
leg.from.lat + (leg.to.lat - leg.from.lat) * f,
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
leg.points = pts;
|
|
143
|
+
}
|
|
144
|
+
const k = stagger(t, i, state.paths.length, 0.15);
|
|
145
|
+
if (k <= 0) return;
|
|
146
|
+
const upto = Math.max(2, Math.floor(leg.points.length * easeOut(k)));
|
|
147
|
+
ctx.strokeStyle = color;
|
|
148
|
+
ctx.lineWidth = 2.4;
|
|
149
|
+
ctx.lineCap = "round";
|
|
150
|
+
ctx.setLineDash([1, 9]);
|
|
151
|
+
ctx.beginPath();
|
|
152
|
+
drawPath(ctx, globe, leg.points.slice(0, upto));
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
ctx.setLineDash([]);
|
|
155
|
+
const head = globe.project(leg.points[upto - 1][0], leg.points[upto - 1][1]);
|
|
156
|
+
if (!head) return;
|
|
157
|
+
if (k < 1) {
|
|
158
|
+
ctx.font = "17px system-ui";
|
|
159
|
+
ctx.textAlign = "center";
|
|
160
|
+
ctx.fillText(icon, head.x, head.y + 6);
|
|
161
|
+
ctx.textAlign = "left";
|
|
162
|
+
} else {
|
|
163
|
+
ctx.fillStyle = color;
|
|
164
|
+
ctx.beginPath();
|
|
165
|
+
ctx.arc(head.x, head.y, 4, 0, TAU);
|
|
166
|
+
ctx.fill();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A progress readout that counts a set of locations in.
|
|
174
|
+
* Ports the readout half of catalogue effect AC.
|
|
175
|
+
*/
|
|
176
|
+
export const tally = ({ caption = "LOCATIONS", duration = 2600, hold = 1600, accent = "#34d399" } = {}) => ({
|
|
177
|
+
name: "tally",
|
|
178
|
+
stage: "above",
|
|
179
|
+
duration,
|
|
180
|
+
hold,
|
|
181
|
+
frame(ctx, globe, t) {
|
|
182
|
+
const h = globe.canvas.clientHeight;
|
|
183
|
+
const total = globe.markers.length;
|
|
184
|
+
readout(ctx, 12, h - 50, caption, String(Math.round(easeOut(t) * total)), { accent });
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Routes fire outward from a hub one at a time, then hold.
|
|
190
|
+
* Ports catalogue effect L.
|
|
191
|
+
*/
|
|
192
|
+
export const arcLaunch = ({
|
|
193
|
+
from,
|
|
194
|
+
to,
|
|
195
|
+
duration = 3600,
|
|
196
|
+
hold = 1200,
|
|
197
|
+
color = "#67e8f9",
|
|
198
|
+
lift = 0.32,
|
|
199
|
+
steps = 48,
|
|
200
|
+
} = {}) => ({
|
|
201
|
+
name: "arcLaunch",
|
|
202
|
+
stage: "above",
|
|
203
|
+
duration,
|
|
204
|
+
hold,
|
|
205
|
+
setup(globe) {
|
|
206
|
+
const hub = from || globe.markers[0];
|
|
207
|
+
const targets = to || globe.markers.slice(1);
|
|
208
|
+
return { hub, targets };
|
|
209
|
+
},
|
|
210
|
+
frame(ctx, globe, t, state) {
|
|
211
|
+
if (!state.hub) return;
|
|
212
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
213
|
+
const lifted = (p, bow) => {
|
|
214
|
+
// Push the point away from the canvas centre to bow the arc off the surface.
|
|
215
|
+
const dx = p.x - w / 2, dy = p.y - h / 2;
|
|
216
|
+
const d = Math.hypot(dx, dy) || 1;
|
|
217
|
+
return { x: p.x + (dx / d) * bow, y: p.y + (dy / d) * bow };
|
|
218
|
+
};
|
|
219
|
+
state.targets.forEach((target, i) => {
|
|
220
|
+
const k = stagger(t, i, state.targets.length, 0.25);
|
|
221
|
+
if (k <= 0) return;
|
|
222
|
+
const reach = easeOut(k);
|
|
223
|
+
ctx.strokeStyle = color;
|
|
224
|
+
ctx.globalAlpha = 0.35 + 0.65 * reach;
|
|
225
|
+
ctx.lineWidth = 1.8;
|
|
226
|
+
ctx.lineCap = "round";
|
|
227
|
+
ctx.beginPath();
|
|
228
|
+
let open = false;
|
|
229
|
+
const upto = Math.max(1, Math.floor(steps * reach));
|
|
230
|
+
for (let s = 0; s <= upto; s++) {
|
|
231
|
+
const f = s / steps;
|
|
232
|
+
const p = globe.project(
|
|
233
|
+
lerp(state.hub.lon, target.lon, f),
|
|
234
|
+
lerp(state.hub.lat, target.lat, f)
|
|
235
|
+
);
|
|
236
|
+
if (!p) {
|
|
237
|
+
open = false;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const bow = lifted(p, Math.sin(f * Math.PI) * lift * 40);
|
|
241
|
+
open ? ctx.lineTo(bow.x, bow.y) : (ctx.moveTo(bow.x, bow.y), (open = true));
|
|
242
|
+
}
|
|
243
|
+
ctx.stroke();
|
|
244
|
+
ctx.globalAlpha = 1;
|
|
245
|
+
const end = globe.project(target.lon, target.lat);
|
|
246
|
+
if (end && reach > 0.98) {
|
|
247
|
+
ctx.fillStyle = color;
|
|
248
|
+
ctx.beginPath();
|
|
249
|
+
ctx.arc(end.x, end.y, 3.2, 0, TAU);
|
|
250
|
+
ctx.fill();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Bars grow out of the surface and settle at their value.
|
|
258
|
+
* Ports catalogue effect N.
|
|
259
|
+
*/
|
|
260
|
+
export const spikesRising = ({
|
|
261
|
+
duration = 2600,
|
|
262
|
+
hold = 1100,
|
|
263
|
+
height = 46,
|
|
264
|
+
width = 2.4,
|
|
265
|
+
color = "#a78bfa",
|
|
266
|
+
} = {}) => ({
|
|
267
|
+
name: "spikesRising",
|
|
268
|
+
stage: "above",
|
|
269
|
+
duration,
|
|
270
|
+
hold,
|
|
271
|
+
frame(ctx, globe, t) {
|
|
272
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
273
|
+
const max = Math.max(1, ...globe.markers.map((m) => m.count || 1));
|
|
274
|
+
ctx.lineCap = "round";
|
|
275
|
+
globe.markers.forEach((m, i) => {
|
|
276
|
+
const grow = easeOut(stagger(t, i, globe.markers.length, 0.35));
|
|
277
|
+
if (grow <= 0) return;
|
|
278
|
+
const p = globe.project(m.lon, m.lat);
|
|
279
|
+
if (!p) return;
|
|
280
|
+
const dx = p.x - w / 2, dy = p.y - h / 2;
|
|
281
|
+
const d = Math.hypot(dx, dy) || 1;
|
|
282
|
+
const ratio = (m.count || 1) / max;
|
|
283
|
+
const len = Math.max(10, ratio * height) * grow;
|
|
284
|
+
const x2 = p.x + (dx / d) * len, y2 = p.y + (dy / d) * len;
|
|
285
|
+
ctx.globalAlpha = 0.22 + grow * 0.78;
|
|
286
|
+
ctx.strokeStyle = m.color || color;
|
|
287
|
+
ctx.shadowColor = m.color || color;
|
|
288
|
+
ctx.shadowBlur = 10;
|
|
289
|
+
ctx.lineWidth = width;
|
|
290
|
+
ctx.beginPath();
|
|
291
|
+
ctx.moveTo(p.x, p.y);
|
|
292
|
+
ctx.lineTo(x2, y2);
|
|
293
|
+
ctx.stroke();
|
|
294
|
+
ctx.shadowBlur = 0;
|
|
295
|
+
ctx.fillStyle = m.color || color;
|
|
296
|
+
ctx.beginPath();
|
|
297
|
+
ctx.arc(x2, y2, Math.max(2.5, width * 1.5), 0, TAU);
|
|
298
|
+
ctx.fill();
|
|
299
|
+
ctx.globalAlpha = 0.34;
|
|
300
|
+
ctx.beginPath();
|
|
301
|
+
ctx.arc(p.x, p.y, 4 + grow * 4, 0, TAU);
|
|
302
|
+
ctx.stroke();
|
|
303
|
+
if (grow > 0.82 && ratio > 0.55) {
|
|
304
|
+
ctx.globalAlpha = grow;
|
|
305
|
+
ctx.fillStyle = "#f8fafc";
|
|
306
|
+
ctx.font = "700 9px Inter, system-ui, sans-serif";
|
|
307
|
+
ctx.textAlign = "center";
|
|
308
|
+
ctx.fillText(String(m.count || 1), x2, y2 - 8);
|
|
309
|
+
}
|
|
310
|
+
ctx.globalAlpha = 1;
|
|
311
|
+
ctx.textAlign = "left";
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Markers fall from above, squash on impact, then settle.
|
|
318
|
+
* Ports catalogue effect S.
|
|
319
|
+
*/
|
|
320
|
+
export const pinDrop = ({
|
|
321
|
+
duration = 3000,
|
|
322
|
+
hold = 900,
|
|
323
|
+
color = "#f97316",
|
|
324
|
+
drop = 70,
|
|
325
|
+
size = 7,
|
|
326
|
+
} = {}) => ({
|
|
327
|
+
name: "pinDrop",
|
|
328
|
+
stage: "above",
|
|
329
|
+
duration,
|
|
330
|
+
hold,
|
|
331
|
+
frame(ctx, globe, t) {
|
|
332
|
+
globe.markers.forEach((m, i) => {
|
|
333
|
+
const k = stagger(t, i, globe.markers.length, 0.4);
|
|
334
|
+
if (k <= 0) return;
|
|
335
|
+
const p = globe.project(m.lon, m.lat);
|
|
336
|
+
if (!p) return;
|
|
337
|
+
const fallPhase = clamp01(k / 0.72);
|
|
338
|
+
const fall = (1 - easeOut(fallPhase)) * -drop;
|
|
339
|
+
const impact = clamp01((k - 0.66) / 0.34);
|
|
340
|
+
const bounce = Math.sin(impact * Math.PI * 2.2) * (1 - impact) * size * 0.8;
|
|
341
|
+
const squash = 1 + Math.sin(impact * Math.PI) * 0.34;
|
|
342
|
+
ctx.save();
|
|
343
|
+
if (impact > 0) {
|
|
344
|
+
ctx.globalAlpha = (1 - impact) * 0.65;
|
|
345
|
+
ctx.strokeStyle = m.color || color;
|
|
346
|
+
ctx.lineWidth = 1.5;
|
|
347
|
+
ctx.beginPath();
|
|
348
|
+
ctx.arc(p.x, p.y, size + impact * 20, 0, TAU);
|
|
349
|
+
ctx.stroke();
|
|
350
|
+
ctx.globalAlpha = 1;
|
|
351
|
+
}
|
|
352
|
+
ctx.translate(p.x, p.y + fall - bounce);
|
|
353
|
+
ctx.scale(squash, 1 / squash);
|
|
354
|
+
ctx.fillStyle = m.color || color;
|
|
355
|
+
ctx.shadowColor = m.color || color;
|
|
356
|
+
ctx.shadowBlur = 14;
|
|
357
|
+
ctx.beginPath();
|
|
358
|
+
ctx.arc(0, -size * 0.9, size, 0, TAU);
|
|
359
|
+
ctx.fill();
|
|
360
|
+
ctx.shadowBlur = 0;
|
|
361
|
+
ctx.beginPath();
|
|
362
|
+
ctx.moveTo(-size * 0.55, -size * 0.25);
|
|
363
|
+
ctx.lineTo(0, size * 1.15);
|
|
364
|
+
ctx.lineTo(size * 0.55, -size * 0.25);
|
|
365
|
+
ctx.closePath();
|
|
366
|
+
ctx.fill();
|
|
367
|
+
ctx.fillStyle = "rgba(255,255,255,.85)";
|
|
368
|
+
ctx.beginPath();
|
|
369
|
+
ctx.arc(0, -size * 0.9, size * 0.34, 0, TAU);
|
|
370
|
+
ctx.fill();
|
|
371
|
+
ctx.restore();
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Circles the camera around a fixed point rather than spinning the globe.
|
|
378
|
+
* Runs beneath so the move lands on the frame being drawn, not the next one.
|
|
379
|
+
* Ports catalogue effect U.
|
|
380
|
+
*/
|
|
381
|
+
export const orbitSubject = ({
|
|
382
|
+
at,
|
|
383
|
+
duration = 7000,
|
|
384
|
+
lon = 26,
|
|
385
|
+
lat = 11,
|
|
386
|
+
} = {}) => ({
|
|
387
|
+
name: "orbitSubject",
|
|
388
|
+
stage: "beneath",
|
|
389
|
+
z: -90,
|
|
390
|
+
duration,
|
|
391
|
+
setup(globe) {
|
|
392
|
+
const subject = at || globe.markers[0] || { lon: globe.lon, lat: globe.lat };
|
|
393
|
+
return { subject };
|
|
394
|
+
},
|
|
395
|
+
frame(_ctx, globe, t, state) {
|
|
396
|
+
const a = t * TAU;
|
|
397
|
+
globe.lon = state.subject.lon + Math.sin(a) * lon;
|
|
398
|
+
globe.lat = state.subject.lat + Math.cos(a) * lat;
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Rings blooming out of every marker on a staggered loop.
|
|
404
|
+
* Ports the pulse half of catalogue effect BA.
|
|
405
|
+
*/
|
|
406
|
+
export const markerBloom = ({
|
|
407
|
+
duration = 4000,
|
|
408
|
+
ring: ringColor = "#f778ba",
|
|
409
|
+
dot = "#58a6ff",
|
|
410
|
+
reach = 22,
|
|
411
|
+
} = {}) => ({
|
|
412
|
+
name: "markerBloom",
|
|
413
|
+
stage: "above",
|
|
414
|
+
duration,
|
|
415
|
+
frame(ctx, globe, t) {
|
|
416
|
+
ctx.globalCompositeOperation = "lighter";
|
|
417
|
+
globe.markers.forEach((m, i) => {
|
|
418
|
+
const p = globe.project(m.lon, m.lat);
|
|
419
|
+
if (!p) return;
|
|
420
|
+
const phase = (t * 2 + i * 0.11) % 1;
|
|
421
|
+
ctx.globalAlpha = (1 - phase) * 0.6;
|
|
422
|
+
ctx.strokeStyle = ringColor;
|
|
423
|
+
ctx.lineWidth = 1.4;
|
|
424
|
+
ctx.beginPath();
|
|
425
|
+
ctx.arc(p.x, p.y, 3 + phase * reach, 0, TAU);
|
|
426
|
+
ctx.stroke();
|
|
427
|
+
ctx.globalAlpha = 1;
|
|
428
|
+
ctx.fillStyle = dot;
|
|
429
|
+
ctx.shadowColor = dot;
|
|
430
|
+
ctx.shadowBlur = 12;
|
|
431
|
+
ctx.beginPath();
|
|
432
|
+
ctx.arc(p.x, p.y, 2.2, 0, TAU);
|
|
433
|
+
ctx.fill();
|
|
434
|
+
ctx.shadowBlur = 0;
|
|
435
|
+
});
|
|
436
|
+
ctx.globalCompositeOperation = "source-over";
|
|
437
|
+
},
|
|
438
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Entrance effects - how the globe arrives. */
|
|
2
|
+
import { clamp01, easeOut, particles, stagger, TAU } from "../runtime.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dots scatter in from off-screen and settle onto the landmasses.
|
|
6
|
+
* Ports catalogue effect A.
|
|
7
|
+
*/
|
|
8
|
+
export const particleAssemble = ({
|
|
9
|
+
duration = 2600,
|
|
10
|
+
hold = 900,
|
|
11
|
+
color = "#67e8f9",
|
|
12
|
+
spacing = 4,
|
|
13
|
+
size = 1.15,
|
|
14
|
+
seed = 11,
|
|
15
|
+
} = {}) => ({
|
|
16
|
+
name: "particleAssemble",
|
|
17
|
+
stage: "above",
|
|
18
|
+
duration,
|
|
19
|
+
hold,
|
|
20
|
+
setup(globe) {
|
|
21
|
+
const points = globe.landPoints(spacing);
|
|
22
|
+
return {
|
|
23
|
+
points,
|
|
24
|
+
seeds: particles(points.length, seed, (random) => ({
|
|
25
|
+
angle: random() * TAU,
|
|
26
|
+
radius: 0.7 + random() * 0.9,
|
|
27
|
+
delay: random() * 0.45,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
frame(ctx, globe, t, { points, seeds }) {
|
|
32
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
33
|
+
ctx.fillStyle = color;
|
|
34
|
+
for (let i = 0; i < points.length; i++) {
|
|
35
|
+
const p = globe.project(points[i][0], points[i][1]);
|
|
36
|
+
if (!p) continue;
|
|
37
|
+
const s = seeds[i];
|
|
38
|
+
const k = easeOut(clamp01((t - s.delay) / (1 - s.delay)));
|
|
39
|
+
const fromX = w / 2 + Math.cos(s.angle) * w * s.radius;
|
|
40
|
+
const fromY = h / 2 + Math.sin(s.angle) * h * s.radius;
|
|
41
|
+
ctx.globalAlpha = 0.25 + k * 0.75;
|
|
42
|
+
ctx.beginPath();
|
|
43
|
+
ctx.arc(fromX + (p.x - fromX) * k, fromY + (p.y - fromY) * k, size, 0, TAU);
|
|
44
|
+
ctx.fill();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A sweep hand rotates once and leaves the map drawn behind it.
|
|
51
|
+
* Ports catalogue effect B.
|
|
52
|
+
*/
|
|
53
|
+
export const radarSweep = ({ duration = 2800, hold = 700, color = "#38bdf8" } = {}) => ({
|
|
54
|
+
name: "radarSweep",
|
|
55
|
+
stage: "post",
|
|
56
|
+
duration,
|
|
57
|
+
hold,
|
|
58
|
+
frame(ctx, globe, t) {
|
|
59
|
+
const w = globe.canvas.clientWidth, h = globe.canvas.clientHeight;
|
|
60
|
+
const cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.52;
|
|
61
|
+
const angle = t * TAU;
|
|
62
|
+
|
|
63
|
+
ctx.globalCompositeOperation = "destination-in";
|
|
64
|
+
ctx.beginPath();
|
|
65
|
+
ctx.moveTo(cx, cy);
|
|
66
|
+
ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + angle);
|
|
67
|
+
ctx.closePath();
|
|
68
|
+
ctx.fill();
|
|
69
|
+
|
|
70
|
+
ctx.globalCompositeOperation = "source-over";
|
|
71
|
+
const a = -Math.PI / 2 + angle;
|
|
72
|
+
const sweep = ctx.createLinearGradient(cx, cy, cx + Math.cos(a) * r, cy + Math.sin(a) * r);
|
|
73
|
+
sweep.addColorStop(0, "transparent");
|
|
74
|
+
sweep.addColorStop(1, color);
|
|
75
|
+
ctx.strokeStyle = sweep;
|
|
76
|
+
ctx.lineWidth = 2;
|
|
77
|
+
ctx.beginPath();
|
|
78
|
+
ctx.moveTo(cx, cy);
|
|
79
|
+
ctx.lineTo(cx + Math.cos(a) * r, cy + Math.sin(a) * r);
|
|
80
|
+
ctx.stroke();
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Markers land one after another with a slight overshoot.
|
|
86
|
+
* Ports catalogue effect K.
|
|
87
|
+
*/
|
|
88
|
+
export const markerCascade = ({ duration = 3400, hold = 900, color = "#34d399" } = {}) => ({
|
|
89
|
+
name: "markerCascade",
|
|
90
|
+
stage: "above",
|
|
91
|
+
duration,
|
|
92
|
+
hold,
|
|
93
|
+
frame(ctx, globe, t) {
|
|
94
|
+
const markers = globe.markers;
|
|
95
|
+
const max = Math.max(1, ...markers.map((m) => m.count || 1));
|
|
96
|
+
markers.forEach((m, i) => {
|
|
97
|
+
const k = stagger(t, i, markers.length, 0.3);
|
|
98
|
+
if (k <= 0) return;
|
|
99
|
+
const p = globe.project(m.lon, m.lat);
|
|
100
|
+
if (!p) return;
|
|
101
|
+
const grow = 1 + 2.7 * Math.pow(k - 1, 3) + 1.7 * Math.pow(k - 1, 2);
|
|
102
|
+
const r = (3 + ((m.count || 1) / max) * 10) * Math.max(0, grow);
|
|
103
|
+
ctx.fillStyle = m.color || color;
|
|
104
|
+
ctx.shadowColor = m.color || color;
|
|
105
|
+
ctx.shadowBlur = 12;
|
|
106
|
+
ctx.beginPath();
|
|
107
|
+
ctx.arc(p.x, p.y, r, 0, TAU);
|
|
108
|
+
ctx.fill();
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
});
|