@aics/vole-core 3.12.4

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.
Files changed (141) hide show
  1. package/LICENSE.txt +26 -0
  2. package/README.md +119 -0
  3. package/es/Atlas2DSlice.js +224 -0
  4. package/es/Channel.js +264 -0
  5. package/es/FileSaver.js +31 -0
  6. package/es/FusedChannelData.js +192 -0
  7. package/es/Histogram.js +250 -0
  8. package/es/ImageInfo.js +127 -0
  9. package/es/Light.js +74 -0
  10. package/es/Lut.js +500 -0
  11. package/es/MarchingCubes.js +507 -0
  12. package/es/MeshVolume.js +334 -0
  13. package/es/NaiveSurfaceNets.js +251 -0
  14. package/es/PathTracedVolume.js +482 -0
  15. package/es/RayMarchedAtlasVolume.js +250 -0
  16. package/es/RenderToBuffer.js +31 -0
  17. package/es/ThreeJsPanel.js +633 -0
  18. package/es/Timing.js +28 -0
  19. package/es/TrackballControls.js +538 -0
  20. package/es/View3d.js +848 -0
  21. package/es/Volume.js +352 -0
  22. package/es/VolumeCache.js +161 -0
  23. package/es/VolumeDims.js +16 -0
  24. package/es/VolumeDrawable.js +702 -0
  25. package/es/VolumeMaker.js +101 -0
  26. package/es/VolumeRenderImpl.js +1 -0
  27. package/es/VolumeRenderSettings.js +203 -0
  28. package/es/constants/basicShaders.js +29 -0
  29. package/es/constants/colors.js +59 -0
  30. package/es/constants/denoiseShader.js +43 -0
  31. package/es/constants/lights.js +42 -0
  32. package/es/constants/materials.js +85 -0
  33. package/es/constants/pathtraceOutputShader.js +13 -0
  34. package/es/constants/scaleBarSVG.js +21 -0
  35. package/es/constants/time.js +34 -0
  36. package/es/constants/volumePTshader.js +153 -0
  37. package/es/constants/volumeRayMarchShader.js +123 -0
  38. package/es/constants/volumeSliceShader.js +115 -0
  39. package/es/index.js +21 -0
  40. package/es/loaders/IVolumeLoader.js +131 -0
  41. package/es/loaders/JsonImageInfoLoader.js +255 -0
  42. package/es/loaders/OmeZarrLoader.js +495 -0
  43. package/es/loaders/OpenCellLoader.js +65 -0
  44. package/es/loaders/RawArrayLoader.js +89 -0
  45. package/es/loaders/TiffLoader.js +219 -0
  46. package/es/loaders/VolumeLoadError.js +44 -0
  47. package/es/loaders/VolumeLoaderUtils.js +221 -0
  48. package/es/loaders/index.js +40 -0
  49. package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +143 -0
  50. package/es/loaders/zarr_utils/WrappedStore.js +51 -0
  51. package/es/loaders/zarr_utils/types.js +24 -0
  52. package/es/loaders/zarr_utils/utils.js +225 -0
  53. package/es/loaders/zarr_utils/validation.js +49 -0
  54. package/es/test/ChunkPrefetchIterator.test.js +208 -0
  55. package/es/test/RequestQueue.test.js +442 -0
  56. package/es/test/SubscribableRequestQueue.test.js +244 -0
  57. package/es/test/VolumeCache.test.js +118 -0
  58. package/es/test/VolumeRenderSettings.test.js +71 -0
  59. package/es/test/lut.test.js +671 -0
  60. package/es/test/num_utils.test.js +140 -0
  61. package/es/test/volume.test.js +98 -0
  62. package/es/test/zarr_utils.test.js +358 -0
  63. package/es/types/Atlas2DSlice.d.ts +41 -0
  64. package/es/types/Channel.d.ts +44 -0
  65. package/es/types/FileSaver.d.ts +6 -0
  66. package/es/types/FusedChannelData.d.ts +26 -0
  67. package/es/types/Histogram.d.ts +57 -0
  68. package/es/types/ImageInfo.d.ts +87 -0
  69. package/es/types/Light.d.ts +27 -0
  70. package/es/types/Lut.d.ts +67 -0
  71. package/es/types/MarchingCubes.d.ts +53 -0
  72. package/es/types/MeshVolume.d.ts +40 -0
  73. package/es/types/NaiveSurfaceNets.d.ts +11 -0
  74. package/es/types/PathTracedVolume.d.ts +65 -0
  75. package/es/types/RayMarchedAtlasVolume.d.ts +41 -0
  76. package/es/types/RenderToBuffer.d.ts +17 -0
  77. package/es/types/ThreeJsPanel.d.ts +107 -0
  78. package/es/types/Timing.d.ts +11 -0
  79. package/es/types/TrackballControls.d.ts +51 -0
  80. package/es/types/View3d.d.ts +357 -0
  81. package/es/types/Volume.d.ts +152 -0
  82. package/es/types/VolumeCache.d.ts +43 -0
  83. package/es/types/VolumeDims.d.ts +28 -0
  84. package/es/types/VolumeDrawable.d.ts +108 -0
  85. package/es/types/VolumeMaker.d.ts +49 -0
  86. package/es/types/VolumeRenderImpl.d.ts +22 -0
  87. package/es/types/VolumeRenderSettings.d.ts +98 -0
  88. package/es/types/constants/basicShaders.d.ts +4 -0
  89. package/es/types/constants/colors.d.ts +2 -0
  90. package/es/types/constants/denoiseShader.d.ts +40 -0
  91. package/es/types/constants/lights.d.ts +38 -0
  92. package/es/types/constants/materials.d.ts +20 -0
  93. package/es/types/constants/pathtraceOutputShader.d.ts +11 -0
  94. package/es/types/constants/scaleBarSVG.d.ts +2 -0
  95. package/es/types/constants/time.d.ts +19 -0
  96. package/es/types/constants/volumePTshader.d.ts +137 -0
  97. package/es/types/constants/volumeRayMarchShader.d.ts +117 -0
  98. package/es/types/constants/volumeSliceShader.d.ts +109 -0
  99. package/es/types/glsl.d.js +0 -0
  100. package/es/types/index.d.ts +28 -0
  101. package/es/types/loaders/IVolumeLoader.d.ts +113 -0
  102. package/es/types/loaders/JsonImageInfoLoader.d.ts +80 -0
  103. package/es/types/loaders/OmeZarrLoader.d.ts +87 -0
  104. package/es/types/loaders/OpenCellLoader.d.ts +9 -0
  105. package/es/types/loaders/RawArrayLoader.d.ts +33 -0
  106. package/es/types/loaders/TiffLoader.d.ts +45 -0
  107. package/es/types/loaders/VolumeLoadError.d.ts +18 -0
  108. package/es/types/loaders/VolumeLoaderUtils.d.ts +38 -0
  109. package/es/types/loaders/index.d.ts +22 -0
  110. package/es/types/loaders/zarr_utils/ChunkPrefetchIterator.d.ts +22 -0
  111. package/es/types/loaders/zarr_utils/WrappedStore.d.ts +24 -0
  112. package/es/types/loaders/zarr_utils/types.d.ts +94 -0
  113. package/es/types/loaders/zarr_utils/utils.d.ts +23 -0
  114. package/es/types/loaders/zarr_utils/validation.d.ts +7 -0
  115. package/es/types/test/ChunkPrefetchIterator.test.d.ts +1 -0
  116. package/es/types/test/RequestQueue.test.d.ts +1 -0
  117. package/es/types/test/SubscribableRequestQueue.test.d.ts +1 -0
  118. package/es/types/test/VolumeCache.test.d.ts +1 -0
  119. package/es/types/test/VolumeRenderSettings.test.d.ts +1 -0
  120. package/es/types/test/lut.test.d.ts +1 -0
  121. package/es/types/test/num_utils.test.d.ts +1 -0
  122. package/es/types/test/volume.test.d.ts +1 -0
  123. package/es/types/test/zarr_utils.test.d.ts +1 -0
  124. package/es/types/types.d.ts +115 -0
  125. package/es/types/utils/RequestQueue.d.ts +112 -0
  126. package/es/types/utils/SubscribableRequestQueue.d.ts +52 -0
  127. package/es/types/utils/num_utils.d.ts +43 -0
  128. package/es/types/workers/VolumeLoaderContext.d.ts +106 -0
  129. package/es/types/workers/types.d.ts +101 -0
  130. package/es/types/workers/util.d.ts +3 -0
  131. package/es/types.js +75 -0
  132. package/es/typings.d.js +0 -0
  133. package/es/utils/RequestQueue.js +267 -0
  134. package/es/utils/SubscribableRequestQueue.js +187 -0
  135. package/es/utils/num_utils.js +231 -0
  136. package/es/workers/FetchTiffWorker.js +153 -0
  137. package/es/workers/VolumeLoadWorker.js +129 -0
  138. package/es/workers/VolumeLoaderContext.js +271 -0
  139. package/es/workers/types.js +41 -0
  140. package/es/workers/util.js +8 -0
  141. package/package.json +83 -0
package/es/Lut.js ADDED
@@ -0,0 +1,500 @@
1
+ import { getColorByChannelIndex } from "./constants/colors.js";
2
+ function clamp(val, cmin, cmax) {
3
+ return Math.min(Math.max(cmin, val), cmax);
4
+ }
5
+ function lerp(xmin, xmax, a) {
6
+ return a * (xmax - xmin) + xmin;
7
+ }
8
+
9
+ // We have an intensity value that is in the range of valueMin to valueMax.
10
+ // This domain is assumed to have been remapped from oldMin to oldMax.
11
+ // We now wish to find the intensity value that corresponds to the same relative position in the new domain of newMin to newMax.
12
+ // For our Luts valueMin will always be 0, and valueMax will always be 255.
13
+ // oldMin and oldMax will be the domain of the original raw data intensities.
14
+ // newMin and newMax will be the domain of the new raw data intensities.
15
+ function remapDomain(value, valueMin, valueMax, oldMin, oldMax, newMin, newMax) {
16
+ const pctOfRange = (value - valueMin) / (valueMax - valueMin);
17
+ const newValue = (newMax - newMin) * pctOfRange + newMin;
18
+ // now locate this value as a relative index in the old range
19
+ const pctOfOldRange = (newValue - oldMin) / (oldMax - oldMin);
20
+ const remapped = valueMin + pctOfOldRange * (valueMax - valueMin);
21
+ return remapped;
22
+ }
23
+
24
+ // We have an intensity value that is in the range of valueMin to valueMax.
25
+ // The input value range is assumed to represent absolute intensity range oldMin to oldMax.
26
+ // We now wish to find the new position of this intensity value
27
+ // when the valueMin-valueMax represents absolute range newMin to newMax
28
+ // After the remapping, the intensity value will be in the range of valueMin to valueMax.
29
+ // For our Luts valueMin will always be 0, and valueMax will always be 255.
30
+ // oldMin and oldMax will be the domain of the original raw data intensities.
31
+ // newMin and newMax will be the domain of the new raw data intensities.
32
+ function remapDomainForCP(value, valueMin, valueMax, oldMin, oldMax, newMin, newMax) {
33
+ const pctOfRange = (value - valueMin) / (valueMax - valueMin);
34
+ // find abs intensity from old range
35
+ const iOld = (oldMax - oldMin) * pctOfRange + oldMin;
36
+ // now locate this value as a relative index in the new range
37
+ const pctOfNewRange = (iOld - newMin) / (newMax - newMin);
38
+ const remapped = valueMin + pctOfNewRange * (valueMax - valueMin);
39
+ return remapped;
40
+ }
41
+ export const LUT_ENTRIES = 256;
42
+ export const LUT_ARRAY_LENGTH = LUT_ENTRIES * 4;
43
+
44
+ // @param {ControlPoint[]} controlPoints - array of {x:number 0..255, opacity:number 0..1, color:array of 3 numbers 0..255}
45
+ // @return {Uint8Array} array of length len*4 representing the rgba values of the gradient
46
+ function arrayFromControlPoints(controlPoints) {
47
+ // current assumption is that control point X values are in the range 0-255
48
+ // and they will be used directly as indices into the LUT.
49
+ // therefore the lut must have 256 entries. Anything else and we have to remap the control points.
50
+ // TODO allow luts that have more or less entries.
51
+ const len = LUT_ENTRIES;
52
+ const lut = new Uint8Array(len * 4).fill(0);
53
+ if (controlPoints.length === 0) {
54
+ return lut;
55
+ }
56
+
57
+ // ensure they are sorted in ascending order of x
58
+ controlPoints.sort((a, b) => a.x - b.x);
59
+
60
+ // special case only one control point.
61
+ if (controlPoints.length === 1) {
62
+ const rgba = controlPointToRGBA(controlPoints[0]);
63
+ // lut was already filled with zeros
64
+ // copy val from x to 255.
65
+ const startx = clamp(controlPoints[0].x, 0, 255);
66
+ for (let x = startx; x < len; ++x) {
67
+ lut[x * 4 + 0] = rgba[0];
68
+ lut[x * 4 + 1] = rgba[1];
69
+ lut[x * 4 + 2] = rgba[2];
70
+ lut[x * 4 + 3] = rgba[3];
71
+ }
72
+ return lut;
73
+ }
74
+ let c0 = controlPoints[0];
75
+ let c1 = controlPoints[1];
76
+ let color0 = controlPointToRGBA(c0);
77
+ let color1 = controlPointToRGBA(c1);
78
+ let lastIndex = 1;
79
+ let a = 0;
80
+ for (let i = 0; i < len; ++i) {
81
+ // find the two control points that i is between
82
+ while (i > c1.x) {
83
+ // advance control points
84
+ c0 = c1;
85
+ color0 = color1;
86
+ lastIndex++;
87
+ if (lastIndex >= controlPoints.length) {
88
+ // if the last control point is before 255, then we want to continue its value all the way to 255.
89
+ c1 = {
90
+ x: 255,
91
+ color: c1.color,
92
+ opacity: c1.opacity
93
+ };
94
+ } else {
95
+ c1 = controlPoints[lastIndex];
96
+ }
97
+ color1 = controlPointToRGBA(c1);
98
+ }
99
+ // find the lerp amount between the two control points
100
+ if (c1.x === c0.x) {
101
+ // use c1
102
+ a = 1.0;
103
+ } else {
104
+ a = (i - c0.x) / (c1.x - c0.x);
105
+ }
106
+ lut[i * 4 + 0] = clamp(lerp(color0[0], color1[0], a), 0, 255);
107
+ lut[i * 4 + 1] = clamp(lerp(color0[1], color1[1], a), 0, 255);
108
+ lut[i * 4 + 2] = clamp(lerp(color0[2], color1[2], a), 0, 255);
109
+ lut[i * 4 + 3] = clamp(lerp(color0[3], color1[3], a), 0, 255);
110
+ }
111
+ return lut;
112
+ }
113
+
114
+ /**
115
+ * @typedef {Object} ControlPoint Used for the TF (transfer function) editor GUI.
116
+ * Need to be converted to LUT for rendering.
117
+ * @property {number} x The X Coordinate: an intensity value, normalized to the 0-255 range
118
+ * @property {number} opacity The Opacity, from 0 to 1
119
+ * @property {Array.<number>} color The Color, 3 numbers from 0-255 for r,g,b
120
+ */
121
+
122
+ function controlPointToRGBA(controlPoint) {
123
+ return [controlPoint.color[0], controlPoint.color[1], controlPoint.color[2], Math.floor(controlPoint.opacity * 255)];
124
+ }
125
+
126
+ // the intensity range will be 0-255,
127
+ // which currently represents the range of the raw data. (not the dtype range)
128
+ const createFullRangeControlPoints = (opacityMin = 0, opacityMax = 1) => [{
129
+ x: 0,
130
+ opacity: opacityMin,
131
+ color: [255, 255, 255]
132
+ }, {
133
+ x: 255,
134
+ opacity: opacityMax,
135
+ color: [255, 255, 255]
136
+ }];
137
+
138
+ /**
139
+ * @typedef {Object} Lut Used for rendering. The start and end of the Lut represent the min and max of the data.
140
+ * @property {Array.<number>} lut LUT_ARRAY_LENGTH element lookup table as array
141
+ * (maps scalar intensity to a rgb color plus alpha, with each value from 0-255)
142
+ * @property {Array.<ControlPoint>} controlPoints
143
+ */
144
+ export class Lut {
145
+ constructor() {
146
+ this.lut = new Uint8Array(LUT_ARRAY_LENGTH);
147
+ this.controlPoints = [];
148
+ this.createFullRange();
149
+ }
150
+
151
+ /**
152
+ * Generate a piecewise linear lookup table that ramps up from 0 to 1 over the b to e domain.
153
+ * If e === b, then we use a step function with f(b) = 0 and f(b + 1) = 1
154
+ * |
155
+ * 1| +---------+-----
156
+ * | /
157
+ * | /
158
+ * | /
159
+ * | /
160
+ * | /
161
+ * 0+=========+---------------+-----
162
+ * 0 b e 255
163
+ * @return {Lut}
164
+ * @param {number} b
165
+ * @param {number} e
166
+ */
167
+ createFromMinMax(b, e) {
168
+ if (e < b) {
169
+ // swap
170
+ const tmp = e;
171
+ e = b;
172
+ b = tmp;
173
+ }
174
+
175
+ // Edge case: b and e are both out of bounds
176
+ if (b < 0 && e < 0) {
177
+ this.controlPoints = createFullRangeControlPoints(1, 1);
178
+ return this.createFromControlPoints(this.controlPoints);
179
+ }
180
+ if (b >= 255 && e >= 255) {
181
+ this.controlPoints = createFullRangeControlPoints(0, 0);
182
+ return this.createFromControlPoints(this.controlPoints);
183
+ }
184
+
185
+ // Generate 2 to 4 control points for a minMax LUT, from left to right
186
+ const controlPoints = [];
187
+
188
+ // Add starting point at x = 0
189
+ let startVal = 0;
190
+ if (b < 0) {
191
+ startVal = -b / (e - b);
192
+ }
193
+ controlPoints.push({
194
+ x: 0,
195
+ opacity: startVal,
196
+ color: [255, 255, 255]
197
+ });
198
+
199
+ // If b > 0, add another point at (b, 0)
200
+ if (b > 0) {
201
+ controlPoints.push({
202
+ x: b,
203
+ opacity: 0,
204
+ color: [255, 255, 255]
205
+ });
206
+ }
207
+
208
+ // If e < 255, Add another point at (e, 1)
209
+ if (e < 255) {
210
+ if (e === b) {
211
+ // Use b + 0.5 as x value instead of e to create a near-vertical ramp
212
+ controlPoints.push({
213
+ x: b + 0.5,
214
+ opacity: 1,
215
+ color: [255, 255, 255]
216
+ });
217
+ } else {
218
+ controlPoints.push({
219
+ x: e,
220
+ opacity: 1,
221
+ color: [255, 255, 255]
222
+ });
223
+ }
224
+ }
225
+
226
+ // Add ending point at x = 255
227
+ let endVal = 1;
228
+ if (e > 255) {
229
+ endVal = (255 - b) / (e - b);
230
+ }
231
+ controlPoints.push({
232
+ x: 255,
233
+ opacity: endVal,
234
+ color: [255, 255, 255]
235
+ });
236
+ return this.createFromControlPoints(controlPoints);
237
+ }
238
+
239
+ // basically, the identity LUT with respect to opacity
240
+ createFullRange() {
241
+ this.controlPoints = createFullRangeControlPoints();
242
+ return this.createFromControlPoints(this.controlPoints);
243
+ }
244
+
245
+ /**
246
+ * Generate a Window/level lookup table
247
+ * @return {Lut}
248
+ * @param {number} wnd in 0..1 range
249
+ * @param {number} lvl in 0..1 range
250
+ */
251
+ createFromWindowLevel(wnd, lvl) {
252
+ // simple linear mapping for actual range
253
+ const b = lvl - wnd * 0.5;
254
+ const e = lvl + wnd * 0.5;
255
+ return this.createFromMinMax(b * 255, e * 255);
256
+ }
257
+
258
+ // @param {Object[]} controlPoints - array of {x:number 0..255, opacity:number 0..1, color:array of 3 numbers 0..255}
259
+ // @return {Uint8Array} array of length 256*4 representing the rgba values of the gradient
260
+ createFromControlPoints(controlPoints) {
261
+ this.lut = arrayFromControlPoints(controlPoints);
262
+ this.controlPoints = controlPoints;
263
+ return this;
264
+ }
265
+
266
+ /**
267
+ * Generate an "equalized" lookup table
268
+ * @return {Lut}
269
+ */
270
+ createFromEqHistogram(histogram) {
271
+ // TODO need to reconcile this if number of histogram bins is not equal to LUT_ENTRIES?
272
+
273
+ const map = [];
274
+ for (let i = 0; i < histogram.getNumBins(); ++i) {
275
+ map[i] = 0;
276
+ }
277
+
278
+ // summed area table?
279
+ map[0] = histogram.getBin(0);
280
+ for (let i = 1; i < histogram.getNumBins(); ++i) {
281
+ map[i] = map[i - 1] + histogram.getBin(i);
282
+ }
283
+ const div = map[map.length - 1] - map[0];
284
+ if (div > 0) {
285
+ // compute lut and track control points for the piecewise linear sections
286
+ const lutControlPoints = [{
287
+ x: 0,
288
+ opacity: 0,
289
+ color: [255, 255, 255]
290
+ }];
291
+ let slope = 0;
292
+ let lastSlope = 0;
293
+ let opacity = 0;
294
+ let lastOpacity = 0;
295
+ for (let i = 1; i < LUT_ENTRIES; ++i) {
296
+ lastOpacity = opacity;
297
+ opacity = clamp(Math.round(255 * (map[i] - map[0])), 0, 255);
298
+ slope = opacity - lastOpacity;
299
+ // if map[i]-map[i-1] is the same as map[i+1]-map[i] then we are in a linear segment and do not need a new control point
300
+ if (slope != lastSlope) {
301
+ lutControlPoints.push({
302
+ x: i - 1,
303
+ opacity: lastOpacity / 255.0,
304
+ color: [255, 255, 255]
305
+ });
306
+ lastSlope = slope;
307
+ }
308
+ }
309
+ lutControlPoints.push({
310
+ x: 255,
311
+ opacity: 1,
312
+ color: [255, 255, 255]
313
+ });
314
+ return this.createFromControlPoints(lutControlPoints);
315
+ } else {
316
+ // just reset to whole range in this case...?
317
+ return this.createFullRange();
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Generate a lookup table with a different color per intensity value.
323
+ * This translates to a unique color per histogram bin with more than zero pixels.
324
+ * TODO THIS IS NOT THE EFFECT WE WANT. Colorize should operate on actual data values, not histogram bins.
325
+ * @return {Lut}
326
+ */
327
+ createLabelColors(histogram) {
328
+ const lut = new Uint8Array(LUT_ARRAY_LENGTH).fill(0);
329
+ const controlPoints = [];
330
+ // assume zero is No Label
331
+ controlPoints.push({
332
+ x: 0,
333
+ opacity: 0,
334
+ color: [0, 0, 0]
335
+ });
336
+ let lastr = 0;
337
+ let lastg = 0;
338
+ let lastb = 0;
339
+ let lasta = 0;
340
+ let r = 0;
341
+ let g = 0;
342
+ let b = 0;
343
+ let a = 0;
344
+
345
+ // assumes exactly one color per bin
346
+ for (let i = 1; i < LUT_ENTRIES; ++i) {
347
+ const ibin = Math.floor(i / (LUT_ENTRIES - 1) * (histogram.getNumBins() - 1));
348
+ if (histogram.getBin(ibin) > 0) {
349
+ const rgb = getColorByChannelIndex(ibin);
350
+ lut[i * 4 + 0] = rgb[0];
351
+ lut[i * 4 + 1] = rgb[1];
352
+ lut[i * 4 + 2] = rgb[2];
353
+ lut[i * 4 + 3] = 255;
354
+ r = rgb[0];
355
+ g = rgb[1];
356
+ b = rgb[2];
357
+ a = 1;
358
+ } else {
359
+ // add a zero control point?
360
+ r = 0;
361
+ g = 0;
362
+ b = 0;
363
+ a = 0;
364
+ // lut was initialized to 0 so no need to set it here.
365
+ }
366
+ // if current control point is same as last one don't add it
367
+ if (r !== lastr || g !== lastg || b !== lastb || a !== lasta) {
368
+ if (lasta === 0) {
369
+ controlPoints.push({
370
+ x: i - 0.5,
371
+ opacity: lasta,
372
+ color: [lastr, lastg, lastb]
373
+ });
374
+ }
375
+ controlPoints.push({
376
+ x: i,
377
+ opacity: a,
378
+ color: [r, g, b]
379
+ });
380
+ lastr = r;
381
+ lastg = g;
382
+ lastb = b;
383
+ lasta = a;
384
+ }
385
+ }
386
+ this.lut = lut;
387
+ this.controlPoints = controlPoints;
388
+ return this;
389
+ }
390
+
391
+ // since this is not a "create" function, it doesn't need to return the object.
392
+ remapDomains(oldMin, oldMax, newMin, newMax) {
393
+ // no attempt is made here to ensure that lut and controlPoints are internally consistent.
394
+ // if they start out consistent, they should end up consistent. And vice versa.
395
+ this.lut = remapLut(this.lut, oldMin, oldMax, newMin, newMax);
396
+ this.controlPoints = remapControlPoints(this.controlPoints, oldMin, oldMax, newMin, newMax);
397
+ }
398
+ }
399
+
400
+ // If the new max is greater than the old max, then
401
+ // the lut's max end will move inward to the left.
402
+ // This is another way of saying that the new max's index is greater than 255 in the old lut
403
+ // If the new min is less than the old min, then
404
+ // the lut's min end will move inward to the right.
405
+ // This is another way of saying that the new min's index is less than 0 in the old lut
406
+ export function remapLut(lut, oldMin, oldMax, newMin, newMax) {
407
+ const newLut = new Uint8Array(LUT_ARRAY_LENGTH);
408
+
409
+ // we will find what intensity is at each index in the new range,
410
+ // and then try to sample the pre-existing lut as if it spans the old range.
411
+ // Build new lut by sampling from old lut.
412
+ for (let i = 0; i < LUT_ENTRIES; ++i) {
413
+ let iOld = remapDomain(i, 0, LUT_ENTRIES - 1, oldMin, oldMax, newMin, newMax);
414
+ if (iOld < 0) {
415
+ iOld = 0;
416
+ }
417
+ if (iOld > LUT_ENTRIES - 1) {
418
+ iOld = LUT_ENTRIES - 1;
419
+ }
420
+ // find the indices above and below for interpolation
421
+ const i0 = Math.floor(iOld);
422
+ const i1 = Math.ceil(iOld);
423
+ const pct = iOld - i0;
424
+
425
+ //console.log(`interpolating ${iOld}: ${lut[i0 * 4 + 3]}, ${lut[i1 * 4 + 3]}, ${pct}`);
426
+ newLut[i * 4 + 0] = Math.round(lerp(lut[i0 * 4 + 0], lut[i1 * 4 + 0], pct));
427
+ newLut[i * 4 + 1] = Math.round(lerp(lut[i0 * 4 + 1], lut[i1 * 4 + 1], pct));
428
+ newLut[i * 4 + 2] = Math.round(lerp(lut[i0 * 4 + 2], lut[i1 * 4 + 2], pct));
429
+ newLut[i * 4 + 3] = Math.round(lerp(lut[i0 * 4 + 3], lut[i1 * 4 + 3], pct));
430
+ }
431
+ return newLut;
432
+ }
433
+ export function remapControlPoints(controlPoints, oldMin, oldMax, newMin, newMax, nudgeEndPoints = true) {
434
+ if (controlPoints.length === 0) {
435
+ return controlPoints;
436
+ }
437
+ const newControlPoints = [];
438
+
439
+ // Save the current position of control points at the ends of the list
440
+ const oldFirstX = controlPoints[0].x;
441
+ const oldLastX = controlPoints[controlPoints.length - 1].x;
442
+
443
+ // assume control point x domain 0-255 is mapped to oldMin-oldMax
444
+
445
+ // remap all cp x values.
446
+ // interpolate all new colors and opacities
447
+ // Do not clip values outside of 0-255. This is important to
448
+ // preserve information for remapping many consecutive times.
449
+ for (let i = 0; i < controlPoints.length; ++i) {
450
+ const cp = controlPoints[i];
451
+ const iOld = remapDomainForCP(cp.x, 0, LUT_ENTRIES - 1, oldMin, oldMax, newMin, newMax);
452
+ const newCP = {
453
+ x: iOld,
454
+ opacity: cp.opacity,
455
+ color: [cp.color[0], cp.color[1], cp.color[2]]
456
+ };
457
+ newControlPoints.push(newCP);
458
+ }
459
+ return nudgeEndPoints ? nudgeRemappedEndControlPoints(newControlPoints, oldFirstX, oldLastX) : newControlPoints;
460
+ }
461
+
462
+ /**
463
+ * Attempts to keep the first and last control points in a remapped list in a sensible place if they were previously on
464
+ * or outside the edge of the range.
465
+ *
466
+ * Commonly (e.g. in the output of nearly all the factory methods in `Lut`), the very first and last control points
467
+ * just define a line of constant opacity out to the upper/lower edge of the range. Remapping these points naively
468
+ * means that the range of the transfer function no longer matches the actual range of intensities. This isn't a
469
+ * problem for producing a lut, but it does make things look weird. If it is possible to do so without losing
470
+ * information, we should try to keep these points in place.
471
+ *
472
+ * In addition to a list of control points, this function requires the x coordinate of the end points _before_
473
+ * remapping, to determine whether the points used to be at or outside the edges of the range.
474
+ */
475
+ function nudgeRemappedEndControlPoints(controlPoints, oldFirstX, oldLastX) {
476
+ const EPSILON = 0.0001;
477
+ const first = controlPoints[0];
478
+ const second = controlPoints[1];
479
+ const secondLast = controlPoints[controlPoints.length - 2];
480
+ const last = controlPoints[controlPoints.length - 1];
481
+ if (Math.abs(first.opacity - (second?.opacity ?? Infinity)) < EPSILON) {
482
+ if (first.x < 0) {
483
+ // control point is now out of bounds - clamp it to 0 (or as close as we can get without losing information)
484
+ first.x = Math.min(0, second.x - 1);
485
+ } else if (oldFirstX < EPSILON) {
486
+ // control point was at or below 0 and has moved inward - snap it to 0 to cover the full range
487
+ first.x = 0;
488
+ }
489
+ }
490
+ if (Math.abs(last.opacity - (secondLast?.opacity ?? Infinity)) < EPSILON) {
491
+ if (last.x > 255) {
492
+ // control point is now out of bounds - clamp it to 255 (or as close as we can get without losing information)
493
+ last.x = Math.max(255, secondLast.x + 1);
494
+ } else if (oldLastX > 255 - EPSILON) {
495
+ // control point was at or above 255 and has moved inward - snap it to 255 to cover the full range
496
+ last.x = 255;
497
+ }
498
+ }
499
+ return controlPoints;
500
+ }