@niivue/niivue 0.44.1 → 0.44.2-dev.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/src/niivue.css CHANGED
@@ -35,7 +35,6 @@ canvas:focus {
35
35
  }
36
36
  div {
37
37
  display: table-row;
38
- background-color: blue;
39
38
  }
40
39
  .dropdown {
41
40
  float: left;
package/src/nvdocument.ts CHANGED
@@ -46,7 +46,8 @@ export enum DRAG_MODE {
46
46
  measurement = 2,
47
47
  pan = 3,
48
48
  slicer3D = 4,
49
- callbackOnly = 5
49
+ callbackOnly = 5,
50
+ roiSelection = 6
50
51
  }
51
52
 
52
53
  // make mutable type
@@ -96,6 +97,7 @@ export type NVConfigOptions = {
96
97
  multiplanarPadPixels: number
97
98
  // @deprecated
98
99
  multiplanarForceRender: boolean
100
+ multiplanarEqualSize: boolean
99
101
  multiplanarShowRender: SHOW_RENDER
100
102
  isRadiologicalConvention: boolean
101
103
  // string to allow infinity
@@ -139,10 +141,20 @@ export type NVConfigOptions = {
139
141
  renderOverlayBlend: number
140
142
  sliceMosaicString: string
141
143
  centerMosaic: boolean
144
+ penSize: number
142
145
  clickToSegment: boolean
143
146
  clickToSegmentRadius: number
144
- clickToSegmentSteps: number
145
147
  clickToSegmentBright: boolean
148
+ clickToSegmentAutoIntensity: boolean // new option, but keep clickToSegmentBright for backwards compatibility
149
+ clickToSegmentIntensityMax: number // also covers NaN
150
+ clickToSegmentIntensityMin: number // also covers NaN
151
+ clickToSegmentPercent: number
152
+ clickToSegmentMaxDistanceMM: number // max distance in mm to consider for click to segment flood fill
153
+ clickToSegmentIs2D: boolean
154
+ // selection box outline thickness
155
+ selectionBoxLineThickness: number
156
+ selectionBoxIsOutline: boolean
157
+ scrollRequiresFocus: boolean
146
158
  }
147
159
 
148
160
  export const DEFAULT_OPTIONS: NVConfigOptions = {
@@ -177,6 +189,7 @@ export const DEFAULT_OPTIONS: NVConfigOptions = {
177
189
  multiplanarPadPixels: 0,
178
190
  // @deprecated
179
191
  multiplanarForceRender: false,
192
+ multiplanarEqualSize: false,
180
193
  multiplanarShowRender: SHOW_RENDER.AUTO, // auto is the same behaviour as multiplanarForceRender: false
181
194
  isRadiologicalConvention: false,
182
195
  meshThicknessOn2D: Infinity,
@@ -210,13 +223,27 @@ export const DEFAULT_OPTIONS: NVConfigOptions = {
210
223
  renderOverlayBlend: 1.0,
211
224
  sliceMosaicString: '',
212
225
  centerMosaic: false,
226
+ penSize: 1, // in voxels, since all drawing is done using bitmap indices
213
227
  clickToSegment: false,
214
- clickToSegmentRadius: 2,
215
- clickToSegmentSteps: 10,
216
- clickToSegmentBright: true
228
+ clickToSegmentRadius: 3, // in mm
229
+ clickToSegmentBright: true,
230
+ clickToSegmentAutoIntensity: false, // new option, but keep clickToSegmentBright for backwards compatibility
231
+ clickToSegmentIntensityMax: NaN, // NaN will use auto threshold (default flood fill behavior from before)
232
+ clickToSegmentIntensityMin: NaN, // NaN will use auto threshold (default flood fill behavior from before)
233
+ // 0 will use auto threshold (default flood fill behavior from before)
234
+ // Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
235
+ // If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
236
+ // for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
237
+ clickToSegmentPercent: 0,
238
+ clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY, // default value is infinity for backwards compatibility with flood fill routine.
239
+ clickToSegmentIs2D: false,
240
+ selectionBoxLineThickness: 4,
241
+ selectionBoxIsOutline: false,
242
+ scrollRequiresFocus: false // determines if the cavas need to be focused to scroll
217
243
  }
218
244
 
219
245
  type SceneData = {
246
+ gamma: number
220
247
  azimuth: number
221
248
  elevation: number
222
249
  crosshairPos: vec3
@@ -230,6 +257,7 @@ type SceneData = {
230
257
  }
231
258
 
232
259
  export const INITIAL_SCENE_DATA = {
260
+ gamma: 1.0,
233
261
  azimuth: 110,
234
262
  elevation: 10,
235
263
  crosshairPos: vec3.create(),
@@ -255,6 +283,7 @@ export type Scene = {
255
283
  pan2Dxyzmm: vec4
256
284
  _elevation?: number
257
285
  _azimuth?: number
286
+ gamma?: number
258
287
  }
259
288
 
260
289
  export type DocumentData = {
@@ -566,6 +595,12 @@ export class NVDocument {
566
595
  }
567
596
 
568
597
  data.labels = [...this.data.labels]
598
+
599
+ // remove any handlers
600
+ for (const label of data.labels) {
601
+ delete label.onClick
602
+ }
603
+
569
604
  data.customData = this.customData
570
605
 
571
606
  // volumes
@@ -1631,7 +1631,7 @@ export class NVImage {
1631
1631
  hdr.datatypeCode = NiiDataType.DT_FLOAT64
1632
1632
  break
1633
1633
  default:
1634
- throw new Error('Unsupported NRRD data type: ' + items[0])
1634
+ throw new Error('Unsupported MHA data type: ' + items[0])
1635
1635
  }
1636
1636
  }
1637
1637
  if (line.startsWith('ObjectType') && !items[0].includes('Image')) {
@@ -1653,6 +1653,9 @@ export class NVImage {
1653
1653
  [mat33[2], mat33[5], mat33[8], offset[2]],
1654
1654
  [0, 0, 0, 1]
1655
1655
  ]
1656
+ while (bytes[pos] === 10) {
1657
+ pos++
1658
+ }
1656
1659
  hdr.vox_offset = pos
1657
1660
  if (isDetached && pairedImgData) {
1658
1661
  if (isGz) {
@@ -373,10 +373,13 @@ following conditions are met:
373
373
  return m
374
374
  }
375
375
 
376
- function str2Buffer(str: string): number[] {
376
+ function str2Buffer(str: string, maxLen: number = 80): number[] {
377
377
  // emulate node.js Buffer.from
378
+ // remove characters than could be used for shell expansion
379
+ str = str.replace(/[`$]/g, '')
378
380
  const bytes = []
379
- for (let i = 0; i < str.length; i++) {
381
+ const len = Math.min(maxLen, str.length)
382
+ for (let i = 0; i < len; i++) {
380
383
  const char = str.charCodeAt(i)
381
384
  bytes.push(char & 0xff)
382
385
  }
@@ -384,70 +387,79 @@ function str2Buffer(str: string): number[] {
384
387
  }
385
388
 
386
389
  // save NIfTI header into UINT8 array for saving to disk
387
- export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false): Uint8Array {
390
+ export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false, isInputEndian = false): Uint8Array {
388
391
  const SHORT_SIZE = 2
389
392
  const FLOAT32_SIZE = 4
390
-
393
+ let isLittleEndian = true
394
+ if (isInputEndian) {
395
+ isLittleEndian = hdr.littleEndian
396
+ }
391
397
  const byteArray = new Uint8Array(348)
392
398
  const view = new DataView(byteArray.buffer)
393
399
  // sizeof_hdr
394
- view.setInt32(0, 348, hdr.littleEndian)
400
+ view.setInt32(0, 348, isLittleEndian)
395
401
 
396
402
  // data_type, db_name, extents, session_error, regular are not used
397
-
403
+ // regular set to 'r' (ASCII 114) for Analyze compatibility
404
+ view.setUint8(38, 114)
398
405
  // dim_info
399
406
  view.setUint8(39, hdr.dim_info)
400
407
 
401
408
  // dims
402
409
  for (let i = 0; i < 8; i++) {
403
- view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], hdr.littleEndian)
410
+ view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian)
404
411
  }
405
412
 
406
413
  // intent_p1, intent_p2, intent_p3
407
- view.setFloat32(56, hdr.intent_p1, hdr.littleEndian)
408
- view.setFloat32(60, hdr.intent_p2, hdr.littleEndian)
409
- view.setFloat32(64, hdr.intent_p3, hdr.littleEndian)
414
+ view.setFloat32(56, hdr.intent_p1, isLittleEndian)
415
+ view.setFloat32(60, hdr.intent_p2, isLittleEndian)
416
+ view.setFloat32(64, hdr.intent_p3, isLittleEndian)
410
417
  // intent_code, datatype, bitpix, slice_start
411
- view.setInt16(68, hdr.intent_code, hdr.littleEndian)
418
+ view.setInt16(68, hdr.intent_code, isLittleEndian)
412
419
  if (isDrawing8) {
413
- view.setInt16(70, 2, hdr.littleEndian) // 2 = DT_UINT8
414
- view.setInt16(72, 8, hdr.littleEndian)
420
+ view.setInt16(70, 2, isLittleEndian) // 2 = DT_UINT8
421
+ view.setInt16(72, 8, isLittleEndian)
415
422
  } else {
416
- view.setInt16(70, hdr.datatypeCode, hdr.littleEndian)
417
- view.setInt16(72, hdr.numBitsPerVoxel, hdr.littleEndian)
423
+ view.setInt16(70, hdr.datatypeCode, isLittleEndian)
424
+ view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian)
418
425
  }
419
- view.setInt16(74, hdr.slice_start, hdr.littleEndian)
426
+ view.setInt16(74, hdr.slice_start, isLittleEndian)
420
427
 
421
428
  // pixdim[8], vox_offset, scl_slope, scl_inter
422
429
  for (let i = 0; i < 8; i++) {
423
- view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], hdr.littleEndian)
430
+ view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian)
424
431
  }
425
432
  if (isDrawing8) {
426
- view.setFloat32(108, 352, hdr.littleEndian)
427
- view.setFloat32(112, 1.0, hdr.littleEndian)
428
- view.setFloat32(116, 0.0, hdr.littleEndian)
433
+ view.setFloat32(108, 352, isLittleEndian)
434
+ view.setFloat32(112, 1.0, isLittleEndian)
435
+ view.setFloat32(116, 0.0, isLittleEndian)
429
436
  } else {
430
- view.setFloat32(108, hdr.vox_offset, hdr.littleEndian)
431
- view.setFloat32(112, hdr.scl_slope, hdr.littleEndian)
432
- view.setFloat32(116, hdr.scl_inter, hdr.littleEndian)
437
+ // view.setFloat32(108, hdr.vox_offset, isLittleEndian)
438
+ view.setFloat32(108, 352, isLittleEndian)
439
+ view.setFloat32(112, hdr.scl_slope, isLittleEndian)
440
+ view.setFloat32(116, hdr.scl_inter, isLittleEndian)
433
441
  }
434
442
  // slice_end
435
- view.setInt16(120, hdr.slice_end, hdr.littleEndian)
443
+ view.setInt16(120, hdr.slice_end, isLittleEndian)
436
444
 
437
445
  // slice_code, xyzt_units
438
446
  view.setUint8(122, hdr.slice_code)
439
- view.setUint8(123, hdr.xyzt_units)
447
+ if (hdr.xyzt_units === 0) {
448
+ view.setUint8(123, 10)
449
+ } else {
450
+ view.setUint8(123, hdr.xyzt_units)
451
+ }
440
452
 
441
453
  // cal_max, cal_min, slice_duration, toffset
442
454
  if (isDrawing8) {
443
- view.setFloat32(124, 0, hdr.littleEndian)
444
- view.setFloat32(128, 0, hdr.littleEndian)
455
+ view.setFloat32(124, 0, isLittleEndian)
456
+ view.setFloat32(128, 0, isLittleEndian)
445
457
  } else {
446
- view.setFloat32(124, hdr.cal_max, hdr.littleEndian)
447
- view.setFloat32(128, hdr.cal_min, hdr.littleEndian)
458
+ view.setFloat32(124, hdr.cal_max, isLittleEndian)
459
+ view.setFloat32(128, hdr.cal_min, isLittleEndian)
448
460
  }
449
- view.setFloat32(132, hdr.slice_duration, hdr.littleEndian)
450
- view.setFloat32(136, hdr.toffset, hdr.littleEndian)
461
+ view.setFloat32(132, hdr.slice_duration, isLittleEndian)
462
+ view.setFloat32(136, hdr.toffset, isLittleEndian)
451
463
 
452
464
  // glmax, glmin are unused
453
465
 
@@ -457,27 +469,34 @@ export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false): Uint8Arr
457
469
  // node.js: byteArray.set(Buffer.from(hdr.aux_file), 228);
458
470
  byteArray.set(str2Buffer(hdr.aux_file), 228)
459
471
  // qform_code, sform_code
460
- view.setInt16(252, hdr.qform_code, hdr.littleEndian)
461
- view.setInt16(254, hdr.sform_code, hdr.littleEndian)
472
+ view.setInt16(252, hdr.qform_code, isLittleEndian)
473
+ // if sform unknown, assume NIFTI_XFORM_SCANNER_ANAT
474
+ if (hdr.sform_code < 1 || hdr.sform_code < 1) {
475
+ view.setInt16(254, 1, isLittleEndian)
476
+ } else {
477
+ view.setInt16(254, hdr.sform_code, isLittleEndian)
478
+ }
462
479
 
463
480
  // quatern_b, quatern_c, quatern_d, qoffset_x, qoffset_y, qoffset_z, srow_x[4], srow_y[4], and srow_z[4]
464
- view.setFloat32(256, hdr.quatern_b, hdr.littleEndian)
465
- view.setFloat32(260, hdr.quatern_c, hdr.littleEndian)
466
- view.setFloat32(264, hdr.quatern_d, hdr.littleEndian)
467
- view.setFloat32(268, hdr.qoffset_x, hdr.littleEndian)
468
- view.setFloat32(272, hdr.qoffset_y, hdr.littleEndian)
469
- view.setFloat32(276, hdr.qoffset_z, hdr.littleEndian)
481
+ view.setFloat32(256, hdr.quatern_b, isLittleEndian)
482
+ view.setFloat32(260, hdr.quatern_c, isLittleEndian)
483
+ view.setFloat32(264, hdr.quatern_d, isLittleEndian)
484
+ view.setFloat32(268, hdr.qoffset_x, isLittleEndian)
485
+ view.setFloat32(272, hdr.qoffset_y, isLittleEndian)
486
+ view.setFloat32(276, hdr.qoffset_z, isLittleEndian)
470
487
  const flattened = hdr.affine.flat()
471
488
  // we only want the first three rows
472
489
  for (let i = 0; i < 12; i++) {
473
- view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], hdr.littleEndian)
490
+ view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian)
474
491
  }
475
492
  // node.js https://www.w3schools.com/nodejs/met_buffer_from.asp
476
493
  // intent_name and magic
477
494
  // node.js byteArray.set(Buffer.from(hdr.intent_name), 328);
478
- byteArray.set(str2Buffer(hdr.intent_name), 328)
495
+ // byteArray.set(str2Buffer(hdr.intent_name), 328)
479
496
  // node.js byteArray.set(Buffer.from(hdr.magic), 344);
480
- byteArray.set(str2Buffer(hdr.magic), 344)
497
+ // byteArray.set(str2Buffer(hdr.magic), 344)
498
+ view.setInt32(344, 3222382, true) // "n+1\0"
499
+
481
500
  return byteArray
482
501
  // return byteArray.buffer;
483
502
  }
package/src/nvlabel.ts CHANGED
@@ -10,6 +10,29 @@ export enum LabelLineTerminator {
10
10
  RING = 'ring'
11
11
  }
12
12
 
13
+ export enum LabelAnchorFlag {
14
+ NONE = 0,
15
+ LEFT = 1 << 0,
16
+ CENTER = 1 << 1,
17
+ RIGHT = 1 << 2,
18
+ TOP = 1 << 3,
19
+ MIDDLE = 1 << 4,
20
+ BOTTOM = 1 << 5
21
+ }
22
+
23
+ export enum LabelAnchorPoint {
24
+ NONE = LabelAnchorFlag.NONE,
25
+ TOPLEFT = LabelAnchorFlag.TOP | LabelAnchorFlag.LEFT,
26
+ TOPCENTER = LabelAnchorFlag.TOP | LabelAnchorFlag.CENTER,
27
+ TOPRIGHT = LabelAnchorFlag.TOP | LabelAnchorFlag.RIGHT,
28
+ MIDDLELEFT = LabelAnchorFlag.MIDDLE | LabelAnchorFlag.LEFT,
29
+ MIDDLECENTER = LabelAnchorFlag.MIDDLE | LabelAnchorFlag.CENTER,
30
+ MIDDLERIGHT = LabelAnchorFlag.MIDDLE | LabelAnchorFlag.RIGHT,
31
+ BOTTOMLEFT = LabelAnchorFlag.BOTTOM | LabelAnchorFlag.LEFT,
32
+ BOTTOMCENTER = LabelAnchorFlag.BOTTOM | LabelAnchorFlag.CENTER,
33
+ BOTTOMRIGHT = LabelAnchorFlag.BOTTOM | LabelAnchorFlag.RIGHT
34
+ }
35
+
13
36
  /**
14
37
  * Class representing label style.
15
38
  * @ignore
@@ -65,15 +88,25 @@ export class NVLabel3D {
65
88
  text: string
66
89
  style: NVLabel3DStyle
67
90
  points?: number[] | number[][]
91
+ anchor: LabelAnchorPoint
92
+ onClick?: (label: NVLabel3D) => void
68
93
 
69
94
  /**
70
95
  * @param text - The text of the label
71
96
  * @param style - The style of the label
72
97
  * @param points - An array of points label for label lines
73
98
  */
74
- constructor(text: string, style: NVLabel3DStyle, points?: number[] | number[][]) {
99
+ constructor(
100
+ text: string,
101
+ style: NVLabel3DStyle,
102
+ points?: number[] | number[][],
103
+ anchor?: LabelAnchorPoint,
104
+ onClick?: (label: NVLabel3D) => void
105
+ ) {
75
106
  this.text = text
76
107
  this.style = style
77
108
  this.points = points
109
+ this.anchor = anchor || LabelAnchorPoint.NONE
110
+ this.onClick = onClick
78
111
  }
79
112
  }
@@ -709,6 +709,12 @@ export class NVMeshLoaders {
709
709
  while (lines[pos].length < 1) {
710
710
  pos++
711
711
  } // skip blank lines
712
+ if (lines[pos].startsWith('METADATA')) {
713
+ while (lines[pos].length > 1) {
714
+ pos++
715
+ } // skip until blank line
716
+ pos++
717
+ }
712
718
  items = lines[pos].trim().split(/\s+/)
713
719
  pos++
714
720
  if (items[0].includes('LINES')) {
@@ -909,7 +915,7 @@ export class NVMeshLoaders {
909
915
  layer.values = obj
910
916
  }
911
917
  }
912
- } else if (ext === 'CRV' || ext === 'CURV') {
918
+ } else if (ext === 'CRV' || ext === 'CURV' || ext === 'THICKNESS' || ext === 'AREA') {
913
919
  layer.values = NVMeshLoaders.readCURV(buffer, n_vert)
914
920
  layer.isTransparentBelowCalMin = false
915
921
  } else if (ext === 'GII') {
@@ -935,6 +941,10 @@ export class NVMeshLoaders {
935
941
  layer.values = NVMeshLoaders.readSMP(buffer, n_vert)
936
942
  } else if (ext === 'STC') {
937
943
  layer.values = NVMeshLoaders.readSTC(buffer, n_vert)
944
+ } else if (NVMeshLoaders.isCurv(buffer)) {
945
+ // Unknown layer overlay format - hail mary assume FreeSurfer
946
+ layer.values = NVMeshLoaders.readCURV(buffer, n_vert)
947
+ layer.isTransparentBelowCalMin = false
938
948
  } else {
939
949
  log.warn('Unknown layer overlay format ' + name)
940
950
  return layer
@@ -1123,6 +1133,19 @@ export class NVMeshLoaders {
1123
1133
  return f32
1124
1134
  } // readSTC()
1125
1135
 
1136
+ static isCurv(buffer: ArrayBuffer): boolean {
1137
+ const view = new DataView(buffer) // ArrayBuffer to dataview
1138
+ // ALWAYS big endian
1139
+ const sig0 = view.getUint8(0)
1140
+ const sig1 = view.getUint8(1)
1141
+ const sig2 = view.getUint8(2)
1142
+ if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {
1143
+ utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')
1144
+ return false
1145
+ }
1146
+ return true
1147
+ }
1148
+
1126
1149
  // read freesurfer curv big-endian format
1127
1150
  // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m
1128
1151
  // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm
@@ -719,6 +719,47 @@ void main() {
719
719
  color = lineColor;
720
720
  }`
721
721
 
722
+ export const fragRectOutlineShader = `#version 300 es
723
+ #line 723
724
+ precision highp int;
725
+ precision highp float;
726
+
727
+ uniform vec4 lineColor;
728
+ uniform vec4 leftTopWidthHeight;
729
+ uniform float thickness; // line thickness in pixels
730
+ uniform vec2 canvasWidthHeight;
731
+
732
+ out vec4 color;
733
+
734
+ void main() {
735
+ // fragment position in screen coordinates
736
+ vec2 fragCoord = gl_FragCoord.xy;
737
+
738
+ // canvas height
739
+ float canvasHeight = canvasWidthHeight.y;
740
+
741
+ // 'top' and 'bottom' to match gl_FragCoord.y coordinate system
742
+ float top = canvasHeight - leftTopWidthHeight.y;
743
+ float bottom = top - leftTopWidthHeight.w;
744
+
745
+ // left and right edges
746
+ float left = leftTopWidthHeight.x;
747
+ float right = left + leftTopWidthHeight.z;
748
+
749
+ bool withinLeft = fragCoord.x >= left && fragCoord.x <= left + thickness;
750
+ bool withinRight = fragCoord.x <= right && fragCoord.x >= right - thickness;
751
+ bool withinTop = fragCoord.y <= top && fragCoord.y >= top - thickness;
752
+ bool withinBottom = fragCoord.y >= bottom && fragCoord.y <= bottom + thickness;
753
+
754
+ bool isOutline = withinLeft || withinRight || withinTop || withinBottom;
755
+
756
+ if (isOutline) {
757
+ color = lineColor;
758
+ } else {
759
+ discard;
760
+ }
761
+ }`
762
+
722
763
  export const vertColorbarShader = `#version 300 es
723
764
  #line 490
724
765
  layout(location=0) in vec3 pos;
@@ -1207,11 +1248,11 @@ uniform mat4 mvpMtx;
1207
1248
  uniform mat4 normMtx;
1208
1249
  out vec4 vClr;
1209
1250
  out vec3 vN;
1210
- out vec4 vP;
1251
+ out vec4 vPc;
1211
1252
  void main(void) {
1212
1253
  vec3 lightPosition = vec3(0.0, 0.0, -10.0);
1213
- vP = vec4(pos, 1.0);
1214
- gl_Position = mvpMtx * vP;
1254
+ vPc = mvpMtx * vec4(pos, 1.0);
1255
+ gl_Position = vPc;
1215
1256
  vN = normalize((normMtx * vec4(norm.xyz,1.0)).xyz);
1216
1257
  //vV = -vec3(modelMtx*vec4(pos,1.0));
1217
1258
  vClr = clr;
@@ -1368,6 +1409,46 @@ void main() {
1368
1409
  color = vec4(d3 + s3 + s, opacity);
1369
1410
  }`
1370
1411
 
1412
+ // https://thorium.rocks/media/curves/curvaceous.html
1413
+ export const fragMeshShaderCrevice = `#version 300 es
1414
+ precision highp int;
1415
+ precision highp float;
1416
+ uniform float opacity;
1417
+ in vec4 vClr;
1418
+ in vec3 vN;
1419
+ in vec4 vPc;
1420
+ out vec4 color;
1421
+ void main() {
1422
+ vec3 n = normalize(vN);
1423
+ // Compute curvature
1424
+ vec3 dx = dFdx(n);
1425
+ vec3 dy = dFdy(n);
1426
+ vec3 xneg = n - dx;
1427
+ vec3 xpos = n + dx;
1428
+ vec3 yneg = n - dy;
1429
+ vec3 ypos = n + dy;
1430
+ float depth = length(vPc.xyz);
1431
+ float curv = (cross(xneg, xpos).y - cross(yneg, ypos).x) / depth;
1432
+ //at this stage 0.5 for flat, with valleys dark and ridges bright
1433
+ curv = 1.0 - (curv + 0.5);
1434
+ //clamp
1435
+ curv = min(max(curv, 0.0), 1.0);
1436
+ // easing function
1437
+ curv = pow(curv, 0.5);
1438
+ //modulate ambient and diffuse with curvature
1439
+ vec3 r = vec3(0.0, 0.0, 1.0); //rayDir: for orthographic projections moving in Z direction (no need for normal matrix)
1440
+ float ambient = 0.6;
1441
+ float diffuse = 0.6;
1442
+ float specular = 0.2;
1443
+ float shininess = 10.0;
1444
+ vec3 lightPosition = vec3(0.0, 10.0, -2.0);
1445
+ vec3 l = normalize(lightPosition);
1446
+ float lightNormDot = dot(n, l);
1447
+ vec3 a = vClr.rgb * ambient * curv;
1448
+ vec3 d = max(lightNormDot, 0.0) * vClr.rgb * diffuse;
1449
+ float s = specular * pow(max(dot(reflect(l, n), r), 0.0), shininess);
1450
+ color = vec4(a + d + s, opacity);
1451
+ }`
1371
1452
  // Phong: default
1372
1453
  export const fragMeshShader = `#version 300 es
1373
1454
  precision highp int;
package/src/types.ts CHANGED
@@ -152,3 +152,16 @@ export type NiiVueLocation = {
152
152
  vox: vec3
153
153
  xy: [number, number]
154
154
  }
155
+
156
+ export type SyncOpts = {
157
+ '3d'?: boolean
158
+ '2d'?: boolean
159
+ zoomPan?: boolean
160
+ cal_min?: boolean
161
+ cal_max?: boolean
162
+ gamma?: boolean
163
+ useSliceOffset?: boolean
164
+ sliceType?: boolean
165
+ crosshair?: boolean
166
+ clipPlane?: boolean
167
+ }