-
+ {!onMapPage && (
+
+ )}
`rgba(0,108,102,${(t - 0.8) * 0.5})` // TEAL_RGB
+ : (t: number) => `rgba(82,212,200,${(t - 0.8) * 0.45})`, // TEAL_ON_DARK_RGB
+ dotAlphaBase: isLight ? 0.1 : 0.16,
+ dotAlphaScale: isLight ? 0.55 : 0.72,
+ };
+}
+
export function drawHeroField(
canvas: HTMLCanvasElement,
isLight: boolean,
@@ -21,29 +40,33 @@ export function drawHeroField(
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
- ctx.fillStyle = isLight ? HERO_BASE_COLOR.light : HERO_BASE_COLOR.dark;
+ const tok = readHeroTokens(isLight);
+
+ ctx.fillStyle = tok.baseFill;
ctx.fillRect(0, 0, w, h);
const step = Math.max(17, Math.round(w / 74));
const cols = Math.ceil(w / step) + 1;
const rows = Math.ceil(h / step) + 1;
- const cold = isLight ? COLD_RGB.light : COLD_RGB.dark;
+
+ const cold = tok.coldRgb;
+ const teal = tok.tealRgb;
for (let j = 0; j < rows; j++) {
for (let i = 0; i < cols; i++) {
const x = i * step;
const y = j * step;
+
let n = fbm(i * 0.11 + 0.5, j * 0.13 + 0.5, HERO_SEED);
const dx = x / w - 0.24;
const dy = y / h - 0.78;
const band = Math.max(0, 1 - Math.sqrt(dx * dx + dy * dy) * 1.5);
n = Math.min(1, n * 0.85 + band * 0.5);
+
const t = Math.max(0, (n - 0.34) / 0.66);
if (t <= 0.02) {
- ctx.fillStyle = isLight
- ? "rgba(10,10,10,.05)"
- : "rgba(255,255,255,.045)";
+ ctx.fillStyle = tok.dotFaint;
ctx.beginPath();
ctx.arc(x, y, 0.8, 0, 6.283);
ctx.fill();
@@ -52,17 +75,18 @@ export function drawHeroField(
const r = 0.9 + t * 2.6;
const mix = Math.pow(t, 1.3);
- const cr = Math.round(cold[0] + (TEAL_RGB[0] - cold[0]) * mix);
- const cg = Math.round(cold[1] + (TEAL_RGB[1] - cold[1]) * mix);
- const cb = Math.round(cold[2] + (TEAL_RGB[2] - cold[2]) * mix);
- const a = (isLight ? 0.1 : 0.16) + t * (isLight ? 0.55 : 0.72);
+ const cr = Math.round(cold[0] + (teal[0] - cold[0]) * mix);
+ const cg = Math.round(cold[1] + (teal[1] - cold[1]) * mix);
+ const cb = Math.round(cold[2] + (teal[2] - cold[2]) * mix);
+ const a = tok.dotAlphaBase + t * tok.dotAlphaScale;
+
ctx.fillStyle = `rgba(${cr},${cg},${cb},${a})`;
ctx.beginPath();
ctx.arc(x, y, r, 0, 6.283);
ctx.fill();
if (t > 0.8) {
- ctx.fillStyle = `rgba(0,140,130,${(t - 0.8) * 0.5})`;
+ ctx.fillStyle = tok.tealGlowDark(t);
ctx.beginPath();
ctx.arc(x, y, r * 2.6, 0, 6.283);
ctx.fill();
@@ -70,20 +94,20 @@ export function drawHeroField(
}
}
- ctx.strokeStyle = isLight
- ? "rgba(10,10,10,.05)"
- : "rgba(255,255,255,.05)";
+ ctx.strokeStyle = tok.gridLine;
ctx.lineWidth = 1;
+
for (let gx = 0; gx <= w; gx += w / 12) {
ctx.beginPath();
ctx.moveTo(gx, 0);
ctx.lineTo(gx, h);
ctx.stroke();
}
+
for (let gy = 0; gy <= h; gy += h / 6) {
ctx.beginPath();
ctx.moveTo(0, gy);
ctx.lineTo(w, gy);
ctx.stroke();
}
-}
+}
\ No newline at end of file
diff --git a/src/lib/map/geogrid.ts b/src/lib/map/geogrid.ts
new file mode 100644
index 0000000..36bbe40
--- /dev/null
+++ b/src/lib/map/geogrid.ts
@@ -0,0 +1,41 @@
+import { ZARR_STORE } from "@/lib/constants/store";
+import type { GeoPoint, GridCell } from "@/types/map";
+
+const { grid, dimensions } = ZARR_STORE;
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.min(max, Math.max(min, value));
+}
+
+function snapToAxis(value: number, start: number, step: number, count: number): {
+ index: number;
+ coordinate: number;
+} {
+ const index = clamp(Math.round((value - start) / step), 0, count - 1);
+ return {
+ index,
+ coordinate: start + index * step,
+ };
+}
+
+/** Snap a WGS-84 point to the nearest ESDC 2.5° grid cell. */
+export function geoPointToZarrGrid(point: GeoPoint): GridCell {
+ const lon = snapToAxis(point.lon, grid.lonStart, grid.lonStep, dimensions.lon);
+ const lat = snapToAxis(point.lat, grid.latStart, grid.latStep, dimensions.lat);
+
+ return {
+ lon: lon.coordinate,
+ lat: lat.coordinate,
+ lonIndex: lon.index,
+ latIndex: lat.index,
+ };
+}
+
+export function formatCoordinate(value: number, digits = 3): string {
+ const direction = value >= 0 ? "" : "-";
+ return `${direction}${Math.abs(value).toFixed(digits)}°`;
+}
+
+export function formatGeoPoint(point: GeoPoint, digits = 3): string {
+ return `${formatCoordinate(point.lon, digits)}, ${formatCoordinate(point.lat, digits)}`;
+}
diff --git a/src/lib/map/viewState.ts b/src/lib/map/viewState.ts
new file mode 100644
index 0000000..3a92fd9
--- /dev/null
+++ b/src/lib/map/viewState.ts
@@ -0,0 +1,14 @@
+import type { MapViewState } from "@/types/map";
+
+export const DEFAULT_MAP_VIEW: MapViewState = {
+ longitude: 10,
+ latitude: 30,
+ zoom: 1.4,
+ bearing: 0,
+ pitch: 0,
+};
+
+export const MAP_BASE_STYLES = {
+ dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
+ light: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
+} as const;
diff --git a/src/lib/zarr/store.ts b/src/lib/zarr/store.ts
new file mode 100644
index 0000000..7968058
--- /dev/null
+++ b/src/lib/zarr/store.ts
@@ -0,0 +1,31 @@
+import * as zarr from "zarrita";
+import { ZARR_STORE } from "@/lib/constants/store";
+import type { GridCell } from "@/types/map";
+
+export type ZarrStore = Awaited>;
+
+export async function openZarrStore(url = ZARR_STORE.url) {
+ const raw = new zarr.FetchStore(url);
+ const store = await zarr.withConsolidatedMetadata(raw);
+ return {
+ store,
+ root: zarr.root(store),
+ };
+}
+
+export async function fetchZarrTimeSeries(
+ ds: ZarrStore,
+ grid: GridCell,
+ variable = ZARR_STORE.defaultVariable,
+): Promise<{ values: Float32Array; variable: string; units?: string }> {
+ const array = await zarr.open(ds.root.resolve(variable), { kind: "array" });
+ const result = await zarr.get(array, [null, null, grid.latIndex, grid.lonIndex]); /* days, hours, lat, lon */
+ const units =
+ typeof array.attrs.units === "string" ? array.attrs.units : undefined;
+
+ return {
+ values: result.data as Float32Array,
+ variable,
+ units,
+ };
+}
diff --git a/src/types/map.ts b/src/types/map.ts
new file mode 100644
index 0000000..ec55036
--- /dev/null
+++ b/src/types/map.ts
@@ -0,0 +1,22 @@
+export type GeoPoint = {
+ lon: number;
+ lat: number;
+};
+
+export type GridCell = GeoPoint & {
+ lonIndex: number;
+ latIndex: number;
+};
+
+export type MapSelection = {
+ click: GeoPoint;
+ grid: GridCell;
+};
+
+export type MapViewState = {
+ longitude: number;
+ latitude: number;
+ zoom: number;
+ bearing?: number;
+ pitch?: number;
+};