@fxi/zartigl 0.6.0 → 0.7.1

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 CHANGED
@@ -70,7 +70,7 @@ const z = new Zartigl({
70
70
  timeRange: { trailing: "P1M" },
71
71
  time: new Date("2025-01-01T00:00:00Z"),
72
72
  depth: 0,
73
- settings: { palette: "rdylbu", opacity: 0.9 },
73
+ style: { palette: "rdylbu", raster: { opacity: 0.9 } },
74
74
  geoVideo: { autoplay: false, loop: true, stepsPerSecond: 4 },
75
75
  });
76
76
 
@@ -81,10 +81,76 @@ await z.init();
81
81
  Use `await z.update({ ... })` to change any subset of the active configuration.
82
82
  Pass `layer` and `source` together when both should switch atomically.
83
83
 
84
+ `source` takes a source id or a preference:
85
+
86
+ - `"geovideo"`: fast; surface level and GeoVideo dates only.
87
+ - `"zarr"`: all dates and depths; heavier on the client.
88
+ - `"auto"`: GeoVideo, falling back to Zarr for other dates and depths; the
89
+ layer style always applies. WMTS only when the entry has neither.
90
+ - `"wmts"`: rendered by the server, palette and legend included; no
91
+ particles, and the layer style has no effect.
92
+
84
93
  `init()` and layer/source updates resolve after consolidated metadata and coordinate axes have loaded. Metadata getters are therefore authoritative afterward, including exact values for irregular time axes.
85
94
 
86
95
  The root import does not bundle the catalog presets. Import catalog data from `@fxi/zartigl/catalog` only when you want the built-in catalog.
87
96
 
97
+ ## Layer style
98
+
99
+ A layer style has the same shape everywhere: catalog `defaults`, the
100
+ `Zartigl` `style` option and `update({ style })`, story views, projversor,
101
+ the MapX `ArcoMapLegend` and the snippets the demo exports.
102
+
103
+ ```jsonc
104
+ {
105
+ "palette": "thermal",
106
+ "renderMode": "particles", // or "raster", "raster+particles"
107
+ "particles": {
108
+ "density": 0.03,
109
+ "speed": 1,
110
+ "fade": 0.7,
111
+ "grade": "vibrant",
112
+ "backdrop": { "enabled": true, "color": "#000000", "opacity": 0.3 }
113
+ },
114
+ "raster": { "opacity": 0.9, "logScale": false, "vibrance": 0.2, "colorDomain": null }
115
+ }
116
+ ```
117
+
118
+ `particles.grade` is `"off"`, `"vibrant"`, or a tuned vibrant grade
119
+ `{ exposure, contrast, saturation, highlights }`; it is replaced as a whole.
120
+
121
+ `particles.backdrop` paints a flat color under the particle trails, on the
122
+ valid cells of the field only (the ocean, not the land), so particles stay
123
+ readable over a light basemap. It is off by default; an object is on unless
124
+ `enabled` is false, and its keys left out take the defaults (`color:
125
+ "#000000"`, `#rgb` or `#rrggbb`; `opacity: 0.5`). Like the grade, it is
126
+ replaced as a whole.
127
+
128
+ Keys merge one by one: engine defaults (`LAYER_STYLE_DEFAULTS`), then the
129
+ catalog entry `defaults`, then the engine's own layer style, then the view.
130
+ From `@fxi/zartigl/catalog`: `resolveEntryStyle(entry, ...styles)` gives the
131
+ style a layer is drawn with, `mergeStyles` patches an override,
132
+ `diffStyle` keeps what differs from a resolved style, and `parseLayerStyle`
133
+ validates, with the path of the offending key. `z.getStyle()` returns the
134
+ style drawn, `z.getStyleOverride()` the override.
135
+
136
+ The view style is an override that replaces the previous one:
137
+ `update({ style })` and projversor `apply({ style })` return every style key
138
+ to the layer's own, then apply it (`{}` clears it). Without `style`, the
139
+ override stays, across layers too. Scalar layers take no `particles` and only
140
+ the `"raster"` render mode.
141
+
142
+ `particles.speed` is always the particle speed; playback speed lives
143
+ elsewhere (`geoVideo.stepsPerSecond`, or a projversor view `playback.speed`).
144
+ In projversor, playback also scales the particles by `particleSpeedFactor`:
145
+ 0.325 at 1×, 1 at 5×.
146
+
147
+ `settings` carries engine tuning only (`particleState`,
148
+ `rgba8MaxParticleZoom`). Flat style keys in it (`particleDensity`,
149
+ `opacity`, `colorGrade`…) are deprecated until 1.0: they patch the style
150
+ override, `style` winning, with one warning per key
151
+ (`splitLegacySettings`, `LEGACY_STYLE_SETTINGS`). A custom palette ramp
152
+ stays a setting.
153
+
88
154
  ## Demo
89
155
 
90
156
  The production demo is the default Vite app and is published at
@@ -161,49 +227,23 @@ finishes publishing it; a renderable sparse frame is cached normally.
161
227
 
162
228
  `VectorLayer`, `ScalarLayer`, `GeoVideoLayer`, and `ZarrSource` remain exported for advanced use cases that need direct control over renderer internals. Ordinary MapLibre integrations should prefer `Zartigl`.
163
229
 
164
- ### Projversor and story entries
230
+ ### Entries without MapLibre
165
231
 
166
- Two entries need no MapLibre (`maplibre-gl` is an optional peer):
232
+ Three entries need no MapLibre (`maplibre-gl` is an optional peer):
167
233
 
168
- - `@fxi/zartigl/projversor`: GeoVideo archives on a WebGL2 sphere that morphs
169
- between world projections.
234
+ - `@fxi/zartigl/engine`: GeoVideo manifests, indexes, decoders and archive
235
+ timelines, palettes and color ramps, color grade, backdrop and trail fade,
236
+ WebGL helpers and the shared GLSL chunks as strings (`VIBRANCE_GLSL`,
237
+ `COLOR_GRADE_GLSL`, `RANDOM_GLSL`).
238
+ - `@fxi/zartigl/catalog`: the built-in catalog, named palettes and styles.
170
239
  - `@fxi/zartigl/story`: the story runtime, with JSON documents, localized
171
240
  copy and scroll activation.
172
241
 
173
- ```ts
174
- import { Projversor, ProjversorStoryView } from "@fxi/zartigl/projversor";
175
- import {
176
- StoryScroller,
177
- parseStoryDocuments,
178
- sceneViewId,
179
- } from "@fxi/zartigl/story";
180
-
181
- // Three stacked canvases in a positioned stage. Attach them before
182
- // constructing: the overlay reads its colors (--ocean, --land, --coast,
183
- // --graticule…) from the front canvas computed style. Throws without WebGL2.
184
- const projversor = new Projversor({ stage, back, gl, front });
185
- projversor.setPlaying(true);
186
-
187
- // One view per scene: layer, preset, rotate, zoom, palette, tint, time…
188
- const { story, views } = parseStoryDocuments(storyJson, viewsJson);
189
- const adapter = new ProjversorStoryView(projversor);
190
-
191
- // The section crossing 60% of the viewport height activates its view.
192
- const scroller = new StoryScroller(sections, (index) => {
193
- const id = sceneViewId(story.scenes[index]);
194
- const view = views.views.find((v) => v.id === id);
195
- if (view) {
196
- void adapter.activate(view);
197
- }
198
- }, { line: 0.6 });
199
-
200
- // Later: scroller.destroy(); adapter.destroy();
201
- ```
202
-
203
- `parseProjversorView` validates a view against the projversor layers and
204
- presets. `goTo`, `jumpTo` and `setInset` move the camera, keeping a screen area
205
- free for text. The finer coastline loads in a separate chunk after the first
206
- paint. `src/demo-site/` is a complete scroll story page built on both entries.
242
+ [projversor](https://github.com/fxi/projversor) (`@fxi/projversor`), the
243
+ renderer of GeoVideo archives on a sphere that morphs between world
244
+ projections, is built on these entries in its own repository, with its demos.
245
+ zartigl never imports it. It replaces the former `@fxi/zartigl/projversor`
246
+ entry: the same names are exported by `@fxi/projversor/zartigl`.
207
247
 
208
248
  ## Alternatives And Project Fit
209
249
 
@@ -0,0 +1,79 @@
1
+ const i = {
2
+ exposure: 0.8,
3
+ contrast: 0.6,
4
+ saturation: 0.4,
5
+ highlights: 0.6
6
+ }, l = Object.freeze({
7
+ enabled: !1,
8
+ ...i
9
+ }), s = Object.freeze({
10
+ enabled: !0,
11
+ ...i
12
+ }), u = {
13
+ exposure: [0, 4],
14
+ contrast: [0, 1],
15
+ saturation: [-1, 1],
16
+ highlights: [0, 1]
17
+ }, a = u;
18
+ function f(e) {
19
+ if (!e)
20
+ return { ...l };
21
+ const o = {
22
+ ...s,
23
+ enabled: e.enabled ?? !0
24
+ };
25
+ for (const n of Object.keys(a)) {
26
+ const t = e[n];
27
+ if (typeof t == "number" && Number.isFinite(t)) {
28
+ const [r, b] = a[n];
29
+ o[n] = Math.min(b, Math.max(r, t));
30
+ }
31
+ }
32
+ return o;
33
+ }
34
+ function h(e) {
35
+ return [e.exposure, e.contrast, e.saturation, e.highlights];
36
+ }
37
+ function m(e) {
38
+ return e === void 0 || e === "off" ? { ...l } : e === "vibrant" ? { ...s } : f({ ...e, enabled: !0 });
39
+ }
40
+ function y(e) {
41
+ const o = f(e);
42
+ if (!o.enabled)
43
+ return "off";
44
+ const n = Object.keys(a);
45
+ return n.every((t) => o[t] === s[t]) ? "vibrant" : Object.fromEntries(n.map((t) => [t, o[t]]));
46
+ }
47
+ const c = Object.freeze({
48
+ enabled: !1,
49
+ color: "#000000",
50
+ opacity: 0.5
51
+ }), d = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i, O = (e) => typeof e == "string" && d.test(e);
52
+ function R(e) {
53
+ if (!e)
54
+ return { ...c };
55
+ const o = { ...c, enabled: !0 };
56
+ return typeof e.enabled == "boolean" && (o.enabled = e.enabled), O(e.color) && (o.color = e.color), typeof e.opacity == "number" && Number.isFinite(e.opacity) && (o.opacity = Math.min(1, Math.max(0, e.opacity))), o;
57
+ }
58
+ function p(e) {
59
+ const o = e.color.slice(1), n = o.length === 3 ? o.split("").map((r) => r + r).join("") : o, t = (r) => parseInt(n.slice(r, r + 2), 16) / 255;
60
+ return [
61
+ t(0),
62
+ t(2),
63
+ t(4),
64
+ e.enabled ? e.opacity : 0
65
+ ];
66
+ }
67
+ export {
68
+ c as B,
69
+ l as C,
70
+ R as a,
71
+ p as b,
72
+ h as c,
73
+ y as d,
74
+ s as e,
75
+ u as f,
76
+ m as g,
77
+ O as i,
78
+ f as r
79
+ };
package/dist/catalog.d.ts CHANGED
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Solid color under the particle trails, drawn on valid data cells only: it
3
+ * keeps particles readable over a light basemap. Off by default; an object
4
+ * without `enabled` is on.
5
+ */
6
+ export declare interface BackdropStyle {
7
+ enabled: boolean;
8
+ /** Hex color, `#rgb` or `#rrggbb`. */
9
+ color: string;
10
+ opacity: number;
11
+ }
12
+
1
13
  export declare interface Catalog {
2
14
  schemaVersion: 2;
3
15
  defaultLocale: string;
@@ -15,22 +27,10 @@ export declare interface CatalogEntry {
15
27
  tags?: string[];
16
28
  kind: "scalar" | "vector";
17
29
  sources: CatalogSource[];
18
- defaults: {
30
+ /** Default source, plus the layer's base style. */
31
+ defaults: LayerStyleInput & {
19
32
  sourceId: string;
20
33
  querySourceId?: string;
21
- palette?: string;
22
- renderMode?: "particles" | "raster" | "raster+particles";
23
- particles?: {
24
- density?: number;
25
- speed?: number;
26
- fade?: number;
27
- };
28
- raster?: {
29
- opacity?: number;
30
- logScale?: boolean;
31
- vibrance?: number;
32
- colorDomain?: [number, number];
33
- };
34
34
  };
35
35
  }
36
36
 
@@ -126,6 +126,9 @@ export declare interface CatalogZarrSource extends CatalogSourceBase {
126
126
  variables: CatalogVariables;
127
127
  }
128
128
 
129
+ /** The keys of `style` whose value differs from `base`'s. */
130
+ export declare function diffStyle(style: LayerStyleInput, base: LayerStyle): LayerStyleInput;
131
+
129
132
  export declare function findCatalogEntries(selector: CatalogEntrySelector, data?: Catalog): CatalogEntry[];
130
133
 
131
134
  export declare function formatTime(ms: number): string;
@@ -134,10 +137,89 @@ export declare function formatVertical(v: number, label: string, units?: string)
134
137
 
135
138
  export declare function getCatalogEntry(id: string, data?: Catalog): CatalogEntry | undefined;
136
139
 
140
+ /** Vibrant grade values; each one left out takes the vibrant preset's. */
141
+ export declare interface GradeTuning {
142
+ /** Mid-tone lift [0, 4]. */
143
+ exposure?: number;
144
+ /** S-curve strength [0, 1]. */
145
+ contrast?: number;
146
+ /** Vibrance-weighted saturation [-1, 1]. */
147
+ saturation?: number;
148
+ /** Push of the bright half toward white [0, 1]. */
149
+ highlights?: number;
150
+ }
151
+
152
+ /** Engine fallbacks, under the catalog `defaults`. */
153
+ export declare const LAYER_STYLE_DEFAULTS: Readonly<LayerStyle>;
154
+
155
+ export declare interface LayerStyle {
156
+ /** Palette id; undefined lets the engine pick its own. */
157
+ palette?: string;
158
+ renderMode: StyleRenderMode;
159
+ particles: ParticleStyle;
160
+ raster: RasterStyle;
161
+ }
162
+
163
+ /** A partial style: any key left out takes the value below it. */
164
+ export declare interface LayerStyleInput {
165
+ palette?: string;
166
+ renderMode?: StyleRenderMode;
167
+ particles?: ParticleStyleInput;
168
+ raster?: Partial<RasterStyle>;
169
+ }
170
+
137
171
  export declare type LocalizedText = Record<string, string>;
138
172
 
139
173
  /**
140
- * Preferred render source: GeoVideo, then WMTS (scalar), then Zarr, skipping
174
+ * One partial style from several, later inputs winning key by key: patch an
175
+ * override without resolving defaults.
176
+ */
177
+ export declare function mergeStyles(...inputs: (LayerStyleInput | undefined)[]): LayerStyleInput;
178
+
179
+ /**
180
+ * Validate a style, throwing an error that names the offending path, e.g.
181
+ * `views.swell.config.style.particles.density must be a number in (0, 0.2]`.
182
+ */
183
+ export declare function parseLayerStyle(value: unknown, path: string, options?: ParseLayerStyleOptions): LayerStyleInput;
184
+
185
+ /** Which keys a caller supports, and for which layer kind. */
186
+ export declare interface ParseLayerStyleOptions {
187
+ kind?: "scalar" | "vector";
188
+ /** Keys left out are rejected as unsupported; all when omitted. */
189
+ supports?: {
190
+ palette?: boolean;
191
+ renderMode?: boolean;
192
+ particles?: readonly (keyof ParticleStyle)[];
193
+ raster?: readonly (keyof RasterStyle)[];
194
+ };
195
+ }
196
+
197
+ /** Style keys, for checks against other definitions of the same shape. */
198
+ export declare const PARTICLE_STYLE_KEYS: (keyof ParticleStyle)[];
199
+
200
+ /** Particle trail color grade: a preset, or the vibrant one tuned. */
201
+ export declare type ParticleGrade = "off" | "vibrant" | GradeTuning;
202
+
203
+ export declare interface ParticleStyle {
204
+ /** Particles per device pixel, (0, 0.2]. */
205
+ density: number;
206
+ /** Css px per frame for the fastest value of the field, (0, 10]. */
207
+ speed: number;
208
+ /** Trail length, [0, 1] (longer when higher). */
209
+ fade: number;
210
+ /** Trail color grade; replaced as a whole, not merged. */
211
+ grade: ParticleGrade;
212
+ /** Solid color under the trails; replaced as a whole, not merged. */
213
+ backdrop: BackdropStyle;
214
+ }
215
+
216
+ /** Partial particle style; a partial backdrop takes the defaults' keys. */
217
+ export declare type ParticleStyleInput = Partial<Omit<ParticleStyle, "backdrop">> & {
218
+ backdrop?: Partial<BackdropStyle>;
219
+ };
220
+
221
+ /**
222
+ * Preferred render source: GeoVideo, then Zarr, then WMTS (scalar), skipping
141
223
  * fixed-period sources that cannot show the requested time. Several GeoVideo
142
224
  * periods may coexist; the latest covering one wins. GeoVideo is a
143
225
  * lightweight display transport; Zarr stays the fallback and query source.
@@ -150,8 +232,31 @@ export declare function pickPreferredSource(entry: CatalogEntry, request?: Sourc
150
232
  */
151
233
  export declare function pickSourceByPriority(entry: CatalogEntry, priority: readonly CatalogSource["type"][], request?: SourceTimeRequest): CatalogSource | undefined;
152
234
 
235
+ export declare const RASTER_STYLE_KEYS: (keyof RasterStyle)[];
236
+
237
+ export declare interface RasterStyle {
238
+ opacity: number;
239
+ logScale: boolean;
240
+ /** Vibrance adjustment, [-1, 1]. */
241
+ vibrance: number;
242
+ /** Fixed physical-value domain; null follows the data. */
243
+ colorDomain: [number, number] | null;
244
+ }
245
+
153
246
  export declare function requireCatalogEntry(id: string, data?: Catalog): CatalogEntry;
154
247
 
248
+ /**
249
+ * The style a catalog layer is drawn with: its own style, then `inputs`.
250
+ * Scalar layers draw a raster.
251
+ */
252
+ export declare function resolveEntryStyle(entry: StyledEntry | undefined, ...inputs: (LayerStyleInput | undefined)[]): LayerStyle;
253
+
254
+ /**
255
+ * Merge styles over {@link LAYER_STYLE_DEFAULTS}, later inputs winning key by
256
+ * key. Usual order: catalog `defaults`, engine style, view.
257
+ */
258
+ export declare function resolveLayerStyle(...inputs: (LayerStyleInput | undefined)[]): LayerStyle;
259
+
155
260
  export declare function resolveLocalizedText(value: LocalizedText | undefined, locale: string, defaultLocale?: string): string;
156
261
 
157
262
  export declare function searchCatalog(query: string, options?: CatalogSearchOptions, data?: Catalog): CatalogEntry[];
@@ -170,4 +275,20 @@ export declare interface SourceTimeRequest {
170
275
  end?: number;
171
276
  }
172
277
 
278
+ /** A catalog entry, or anything with its kind and default style. */
279
+ export declare interface StyledEntry {
280
+ kind: "scalar" | "vector";
281
+ defaults?: LayerStyleInput;
282
+ }
283
+
284
+ /** `style` without the keys a layer of `kind` cannot draw. */
285
+ export declare function styleForKind(style: LayerStyleInput, kind: StyledEntry["kind"] | undefined): LayerStyleInput;
286
+
287
+ /**
288
+ * Layer style: how a layer looks, with the same keys in the catalog
289
+ * `defaults`, the engines and story views. Playback (how time moves) is not
290
+ * part of it, so `particles.speed` is always the particle speed.
291
+ */
292
+ export declare type StyleRenderMode = "particles" | "raster" | "raster+particles";
293
+
173
294
  export { }