@geoblocks/cesium-post-process 0.1.0 → 0.1.2

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/weather-map.js ADDED
@@ -0,0 +1,290 @@
1
+ import {Cartesian4, Math as CesiumMath} from '@cesium/core';
2
+ // @ts-expect-error the engine exports its textures, but does not type them
3
+ import {Texture} from '@cesium/engine';
4
+
5
+ const TWO_PI = CesiumMath.TWO_PI;
6
+ // the max map's texels are blocks of this many pixels of the map across
7
+ const POOL = 4;
8
+
9
+ /**
10
+ * Where it rains: an image whose red channel is the local intensity, 0 to 1,
11
+ * over a rectangle of longitudes and latitudes, north up, not crossing the
12
+ * antimeridian. Outside the rectangle there is none, so the image should
13
+ * fade to 0 at its edges.
14
+ * @typedef {{image: HTMLCanvasElement | ImageData | {width: number, height: number, data: Uint8ClampedArray}, rectangle: import('@cesium/core').Rectangle}} WeatherMapOptions
15
+ */
16
+
17
+ /**
18
+ * The pixels of an image, read once.
19
+ * @param {WeatherMapOptions['image']} image
20
+ */
21
+ export function imageData(image) {
22
+ return 'data' in image ? image : image.getContext('2d', {willReadFrequently: true})?.getImageData(0, 0, image.width, image.height);
23
+ }
24
+
25
+ /**
26
+ * A texture's source from an image: a canvas or an ImageData as it is, other pixels as an array,
27
+ * which Cesium flips as it flips an image.
28
+ * @param {HTMLCanvasElement | ImageData | {width: number, height: number, data: Uint8ClampedArray}} image
29
+ */
30
+ function textureSource(image) {
31
+ if (!('data' in image) || (typeof ImageData !== 'undefined' && image instanceof ImageData)) {
32
+ return image;
33
+ }
34
+ const {width, height, data} = image;
35
+ return {width, height, arrayBufferView: new Uint8Array(data.buffer, data.byteOffset, data.length)};
36
+ }
37
+
38
+ /**
39
+ * The highest value of each channel of an image, 0 to 1.
40
+ * @param {{data: Uint8ClampedArray} | undefined} data
41
+ */
42
+ function highest(data) {
43
+ const max = [0, 0, 0, 0];
44
+ const values = data?.data ?? [];
45
+ for (let i = 0; i < values.length; i += 4) {
46
+ for (let channel = 0; channel < 4; channel++) {
47
+ max[channel] = Math.max(max[channel], values[i + channel]);
48
+ }
49
+ }
50
+ return max.map((value) => value / 255);
51
+ }
52
+
53
+ /**
54
+ * The highest value of each channel of images of the same size around each block of POOL x POOL
55
+ * pixels: in the block and its 8 neighbors, as RGBA bytes. A ray sampled less than a block apart
56
+ * then finds every cell of rain it passes, however small.
57
+ * @param {Array<{width: number, height: number, data: Uint8ClampedArray} | undefined>} images
58
+ */
59
+ function maxPool(images) {
60
+ const {width, height} = /** @type {{width: number, height: number}} */ (images[0]);
61
+ const w = Math.ceil(width / POOL);
62
+ const h = Math.ceil(height / POOL);
63
+ const pooled = new Uint8Array(w * h * 4);
64
+ for (const image of images) {
65
+ if (!image) {
66
+ continue;
67
+ }
68
+ for (let y = 0; y < height; y++) {
69
+ for (let x = 0; x < width; x++) {
70
+ const i = 4 * (Math.floor(y / POOL) * w + Math.floor(x / POOL));
71
+ for (let channel = 0; channel < 4; channel++) {
72
+ pooled[i + channel] = Math.max(pooled[i + channel], image.data[4 * (y * width + x) + channel]);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ const data = new Uint8Array(w * h * 4);
78
+ for (let y = 0; y < h; y++) {
79
+ for (let x = 0; x < w; x++) {
80
+ for (let channel = 0; channel < 4; channel++) {
81
+ let max = 0;
82
+ for (let dy = Math.max(y - 1, 0); dy <= Math.min(y + 1, h - 1); dy++) {
83
+ for (let dx = Math.max(x - 1, 0); dx <= Math.min(x + 1, w - 1); dx++) {
84
+ max = Math.max(max, pooled[4 * (dy * w + dx) + channel]);
85
+ }
86
+ }
87
+ data[4 * (y * w + x) + channel] = max;
88
+ }
89
+ }
90
+ }
91
+ return {width: w, height: h, data};
92
+ }
93
+
94
+ /**
95
+ * A map of the rain for the shaders, WeatherMap.glsl's uniforms, and its
96
+ * intensity at a point on the CPU, bilinear as the shaders sample it. Without
97
+ * a map, the intensity everywhere is the empty value: 1 for rain everywhere,
98
+ * 0 for none. A next map, over the same rectangle and of the same size, crossfades from the map to
99
+ * it by the blend, as the radar from one hour to the next.
100
+ */
101
+ export default class WeatherMap {
102
+ /**
103
+ * @param {number} empty the intensity everywhere without a map
104
+ */
105
+ constructor(empty) {
106
+ this.empty_ = empty;
107
+ /** @type {WeatherMapOptions | undefined} */
108
+ this.map_ = undefined;
109
+ /** @type {{width: number, height: number, data: Uint8ClampedArray} | undefined} */
110
+ this.data_ = undefined;
111
+ /** @type {WeatherMapOptions | undefined} */
112
+ this.nextMap_ = undefined;
113
+ /** @type {{width: number, height: number, data: Uint8ClampedArray} | undefined} */
114
+ this.nextData_ = undefined;
115
+ /**
116
+ * From the map, 0, to the next one, 1.
117
+ */
118
+ this.blend = 0;
119
+ /**
120
+ * The rectangle in the ellipsoid's texture coordinates: its west and
121
+ * south, and one over its width and height; the whole globe without a map.
122
+ */
123
+ this.bounds = new Cartesian4(0, 0, 1, 1);
124
+ /**
125
+ * The highest intensity on the map, 0 to 1: the empty value without a map; and the highest
126
+ * value of each of its channels.
127
+ */
128
+ this.maxIntensity = empty;
129
+ this.maxValues = [empty, empty, empty, empty];
130
+ // made when the shaders first need it: a stage given an image makes its texture a frame after
131
+ // it is set, and draws without one in between
132
+ /** @type {any} */
133
+ this.texture_ = undefined;
134
+ /** @type {any} */
135
+ this.nextTexture_ = undefined;
136
+ /** @type {{width: number, height: number, data: Uint8Array} | undefined} */
137
+ this.maxImage_ = undefined;
138
+ /** @type {any} */
139
+ this.maxTexture_ = undefined;
140
+ }
141
+
142
+ get map() {
143
+ return this.map_;
144
+ }
145
+
146
+ set map(value) {
147
+ this.map_ = value;
148
+ if (!value) {
149
+ this.data_ = undefined;
150
+ Cartesian4.fromElements(0, 0, 1, 1, this.bounds);
151
+ this.maxIntensity = this.empty_;
152
+ } else {
153
+ const {image, rectangle} = value;
154
+ this.data_ = imageData(image);
155
+ Cartesian4.fromElements(
156
+ rectangle.west / TWO_PI + 0.5,
157
+ rectangle.south / CesiumMath.PI + 0.5,
158
+ TWO_PI / (rectangle.east - rectangle.west),
159
+ CesiumMath.PI / (rectangle.north - rectangle.south),
160
+ this.bounds,
161
+ );
162
+ }
163
+ this.updateMaxIntensity_();
164
+ // made again from the new image when the shaders next need it
165
+ this.texture_ = this.texture_?.destroy();
166
+ this.clearMaxImage_();
167
+ }
168
+
169
+ get nextMap() {
170
+ return this.nextMap_;
171
+ }
172
+
173
+ set nextMap(value) {
174
+ this.nextMap_ = value;
175
+ this.nextData_ = value && imageData(value.image);
176
+ this.updateMaxIntensity_();
177
+ this.nextTexture_ = this.nextTexture_?.destroy();
178
+ this.clearMaxImage_();
179
+ }
180
+
181
+ clearMaxImage_() {
182
+ this.maxImage_ = undefined;
183
+ this.maxTexture_ = this.maxTexture_?.destroy();
184
+ }
185
+
186
+ /**
187
+ * The highest intensity around each block of the map's pixels, over the map and the next one,
188
+ * made when first needed: undefined without a map.
189
+ */
190
+ maxImage() {
191
+ if (this.map_ && !this.maxImage_) {
192
+ this.maxImage_ = maxPool([this.data_, this.nextData_]);
193
+ }
194
+ return this.maxImage_;
195
+ }
196
+
197
+ updateMaxIntensity_() {
198
+ const map = highest(this.data_);
199
+ const next = highest(this.nextMap_ ? this.nextData_ : undefined);
200
+ this.maxValues = this.map_ ? map.map((value, channel) => Math.max(value, next[channel])) : [this.empty_, this.empty_, this.empty_, this.empty_];
201
+ this.maxIntensity = this.maxValues[0];
202
+ }
203
+
204
+ /**
205
+ * The intensity at a point, 0 to 1, bilinear as the shaders sample it: the
206
+ * empty value without a map, 0 outside its rectangle.
207
+ * @param {number} longitude radians
208
+ * @param {number} latitude radians
209
+ */
210
+ intensityAt(longitude, latitude) {
211
+ if (!this.map_ || !this.data_) {
212
+ return this.empty_;
213
+ }
214
+ const {west, south, east, north} = this.map_.rectangle;
215
+ const u = (longitude - west) / (east - west);
216
+ const v = (latitude - south) / (north - south);
217
+ if (u < 0 || u > 1 || v < 0 || v > 1) {
218
+ return 0;
219
+ }
220
+ const intensity = this.sample_(this.data_, u, v);
221
+ if (!this.nextData_) {
222
+ return intensity;
223
+ }
224
+ return intensity + (this.sample_(this.nextData_, u, v) - intensity) * this.blend;
225
+ }
226
+
227
+ /**
228
+ * An image's intensity, bilinear, at a point of its rectangle, 0 to 1 from its west and south.
229
+ * @param {{width: number, height: number, data: Uint8ClampedArray}} image
230
+ * @param {number} u
231
+ * @param {number} v
232
+ */
233
+ sample_(image, u, v) {
234
+ const {width, height, data} = image;
235
+ // the image's rows go down from the north
236
+ const x = CesiumMath.clamp(u * width - 0.5, 0, width - 1);
237
+ const y = CesiumMath.clamp((1 - v) * height - 0.5, 0, height - 1);
238
+ const x0 = Math.floor(x);
239
+ const y0 = Math.floor(y);
240
+ const x1 = Math.min(x0 + 1, width - 1);
241
+ const y1 = Math.min(y0 + 1, height - 1);
242
+ const red = (/** @type {number} */ column, /** @type {number} */ row) => data[4 * (row * width + column)] / 255;
243
+ const upper = red(x0, y0) + (red(x1, y0) - red(x0, y0)) * (x - x0);
244
+ const lower = red(x0, y1) + (red(x1, y1) - red(x0, y1)) * (x - x0);
245
+ return upper + (lower - upper) * (y - y0);
246
+ }
247
+
248
+ /**
249
+ * The map's texture, a pixel of the empty value without a map.
250
+ * @param {any} context the scene's context
251
+ */
252
+ texture(context) {
253
+ if (!this.texture_) {
254
+ const value = 255 * this.empty_;
255
+ this.texture_ = new Texture({context, source: textureSource(this.map_?.image ?? new ImageData(new Uint8ClampedArray([value, value, value, 255]), 1, 1))});
256
+ }
257
+ return this.texture_;
258
+ }
259
+
260
+ /**
261
+ * The next map's texture, the map's without one.
262
+ * @param {any} context the scene's context
263
+ */
264
+ nextTexture(context) {
265
+ if (!this.nextMap_) {
266
+ return this.texture(context);
267
+ }
268
+ this.nextTexture_ ??= new Texture({context, source: textureSource(this.nextMap_.image)});
269
+ return this.nextTexture_;
270
+ }
271
+
272
+ /**
273
+ * The max map's texture, a pixel of the empty value without a map.
274
+ * @param {any} context the scene's context
275
+ */
276
+ maxTexture(context) {
277
+ if (!this.maxTexture_) {
278
+ const value = 255 * this.empty_;
279
+ const {width, height, data} = this.maxImage() ?? {width: 1, height: 1, data: new Uint8Array([value, value, value, 255])};
280
+ this.maxTexture_ = new Texture({context, source: {width, height, arrayBufferView: data}});
281
+ }
282
+ return this.maxTexture_;
283
+ }
284
+
285
+ destroy() {
286
+ this.texture_ = this.texture_?.destroy();
287
+ this.nextTexture_ = this.nextTexture_?.destroy();
288
+ this.maxTexture_ = this.maxTexture_?.destroy();
289
+ }
290
+ }
package/shaders/Hsl.js DELETED
@@ -1,2 +0,0 @@
1
- // This file is automatically rebuilt from Hsl.glsl.
2
- export default "// Hue (0 to 1, red at 0), saturation and lightness of a color.\nvec3 rgbToHsl(vec3 color) {\n float maxc = max(color.r, max(color.g, color.b));\n float minc = min(color.r, min(color.g, color.b));\n float lightness = 0.5 * (maxc + minc);\n float delta = maxc - minc;\n if (delta < 1e-5) {\n return vec3(0.0, 0.0, lightness);\n }\n float saturation = lightness > 0.5 ? delta / (2.0 - maxc - minc) : delta / (maxc + minc);\n float hue = maxc == color.r ? (color.g - color.b) / delta + (color.g < color.b ? 6.0 : 0.0)\n : maxc == color.g ? (color.b - color.r) / delta + 2.0\n : (color.r - color.g) / delta + 4.0;\n return vec3(hue / 6.0, saturation, lightness);\n}\n";