-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.html
2522 lines (2191 loc) · 95.7 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" /> <!-- needs to appear early -->
<!--
Bloch Simulator for educational interactive MR simulation.
Copyright (c) 2021 Lars G. Hanson, larsh#drcmr.dk, lghan#dtu.dk ( # -> @ )
http://drcmr.dk/larsh, http://www.cmr.healthtech.dtu.dk/
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Please do not redistribute versions yourself except via the channels below, but
instead share changes with the original developer to improve the official releases:
Online version: http://drcmr.dk/bloch
Software repository: https://github.com/larsh957/Bloch-Simulator
Android app: https://play.google.com/store/apps/developer?id=Lars+G.+Hanson
The software is buggy and tested fixes/improved are much appreciated if they impose
no restrictions on software use or redistribution. Since the software development
happens in spare time, bug reports and software suggestions may largely be ignored
(ideas are already abundant, but time for implementation very limited).
-->
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name='viewport' content='user-scalable=0' /> <!-- preventing non-canvas zoom -->
<title>Bloch Simulator for MRI & NMR education</title>
<script src="https://threejs.org/examples/js/libs/stats.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.min.css">
<!-- Check https://learn.jquery.com/jquery-ui/getting-started/ for intro. -->
<!-- jquery-mobile seems not worth the effort in this case. -->
<!-- Unused THREEx game extensions can likely add keyboard control, e.g (search THREEx further down).
<script src="js/THREEx.KeyboardState.js"></script>
<script src="js/THREEx.FullScreen.js"></script>
<script src="js/THREEx.WindowResize.js"></script> //Could possibly have saved some trouble.
-->
<link rel="manifest" href="manifest.json">
</head>
<body style="overflow: hidden">
<script type="module">
// Jquery support of ES6 differs from other imported modules, and fails:
// var $ = require( "https://unpkg.com/jquery/dist/jquery.js" ); or
// import {$,jQuery} from "https://unpkg.com/jquery/dist/jquery.js";
// window.$ = $; // Expose to other imports.
// window.jQuery = jQuery; // Expose to other imports.
// It requires extra require-module and becomes messy.
import * as dat from "https://unpkg.com/dat.gui/build/dat.gui.module.js";
// Apparantly selective modular import makes no difference compared to
import * as THREE from "https://unpkg.com/three/build/three.module.js";
// This list of functions seem to play no role, and size is independent.
// It is commented out since THREE otherwise have to be removed, except
// in webpack-version.
// import { Vector2, Vector3, Color, MeshBasicMaterial, MeshLambertMaterial,
// Matrix4, Quaternion, CylinderBufferGeometry, Mesh, CircleGeometry,
// PlaneGeometry, PlaneBufferGeometry, CircleBufferGeometry,
// PerspectiveCamera, AmbientLight, DirectionalLight,
// DirectionalLightHelper, FontLoader, TextBufferGeometry,
// WebGLRenderer, Scene, AxisHelper} from "https://unpkg.com/three/build/three.module.js";
import { OrbitControls } from "https://unpkg.com/three/examples/jsm/controls/OrbitControls.js"; //imports from three.module.js, so only get Three from there.
import { WEBGL } from "https://unpkg.com/three/examples/jsm/WebGL.js";
"use strict"; // strict mode to avoid implicit globals added to global object "window".
// Variables declared here are added to "window". Prefer variables local to app.
var camera, controls, renderer;
var offsetHeight = 0;
var zoomFactor = 1;
var resizeTimeout;
function adjustToScreen() {
let scrHeight = window.innerHeight;
let scrWidth = window.innerWidth;
let shiftUp = false;
var appFlag = document.URL.indexOf( 'http://' ) === -1 &&
document.URL.indexOf( 'https://' ) === -1; // Is code running as app?
// let AndroidDevice = navigator.userAgent.includes('Android'); //not IE compat
let AndroidDevice = (navigator.userAgent.indexOf('Android') != -1 );
let iOSdevice = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
if ( AndroidDevice || iOSdevice ) {
if ((scrWidth < 800) && (scrWidth < scrHeight)) {
$( "#dialogUseLandscape" ).dialog({
modal: false,
buttons: {
Ok: function() {
$( this ).dialog( "close" );
}}
}).css("font-size", "35px");
shiftUp = true; }
else {
$( "#dialogUseLandscape:visible" ).dialog( "close" );
shiftUp = false;}
if (! appFlag) {
$( "#dialogConsiderApp" ).dialog({
modal: false,
minWidth: 500,
buttons: {
Ok: function() {
$( this ).dialog( "close" );
}}
}).css("font-size", "35px");
}
}
let totalWidth = 5; //5 is left margin
$( '.EventButtons' ).each(function () { //doesnt work if $() is cached
offsetHeight || (offsetHeight = this.offsetHeight); //once!
$(this).css({
left: totalWidth
});
totalWidth += this.offsetWidth + 10;
});
totalWidth -= 5; // Reduce right margin
if ((scrWidth < 800) || (totalWidth > scrWidth))
zoomFactor = scrWidth / totalWidth;
else
zoomFactor = 1;
$( "#EventMenu").css('zoom', zoomFactor);
$( '.EventButtons' ).css('z-index',20).each(function () {
$(this).css({
top: scrHeight/zoomFactor - (1+shiftUp)*offsetHeight - 5
});
});
let iconWidth = $( ".icons" ).width();
$( ".icons" ).css({
bottom: null,
right: null,
top: (scrHeight*0.65)+"px",
left: (scrWidth*0.97 - iconWidth - 2) + "px" //requires width to be set in HTML.
});
if (scrHeight < 600) {
$( "#Saving" ).hide();
} else {
$( "#Saving" ).show();
}
//let socMediaWidth = $( "#socialMedia" ).width()); //too small.
if (scrWidth > (800 + 180)) {
$( "#socialMedia" ).show()
if (scrWidth > (800 + 300))
$( "#specificSocialMedia" ).show()
else
$( "#specificSocialMedia" ).hide(); }
else
$( "#socialMedia" ).hide();
camera.aspect = scrWidth / scrHeight;
camera.updateProjectionMatrix();
renderer.setSize(scrWidth, scrHeight);
if ($( "#newBlochSimulator" ).dialog( "instance" ) &&
$( "#newBlochSimulator" ).dialog( "isOpen" )) { //reopen to center.
$( "#newBlochSimulator" ).dialog( "close" ).dialog( "open" );
}
} // adjustToScreen
function onResize() {
// Throttle resizing. Ignore resize events as long as an adjustToScreen is queued.
if ( !resizeTimeout ) {
resizeTimeout = setTimeout(function() {
resizeTimeout = null;
adjustToScreen();
window.setTimeout(0); //flush cache;
adjustToScreen(); // fire twice since fast width changes may cause small errors.
// The adjustToScreen() will execute at a rate of max 5fps
}, 200);
}
} // onResize
function dialog(id) {
return function () {
$( "#"+id ).dialog({
modal: false//,
// buttons: {
// Ok: function() {
// $( this ).dialog( "close" );
// }}
}); }
}
function launchApp() { // started onload
// Globals: TODO: group them
var debug = false;
//
// Set both reloadSceneResetsParms and hideWhenSelected false to provide two
// levels of scene reload (minimal & full). Or latter true to save screen space:
var reloadSceneResetsParms = false; // Scene main button does full reset.
var hideWhenSelected = false; // Chosen scene label is removed from scene submenu.
// Set Equilibrium menu item visible in HTML part if hideWhenSelected chosen false.
//
var scene;
var eventButtons = $( '.EventButtons'); //cache for speed.
var gui, state, blochMenu;
var dt, lastTime, elapsed, then;
const fpsInterval = 1000/70; //set at 30 to keep more constant rate or free CPU.
// Will never be more than 60 (writing 70 makes it 60, if possible).
const Tmax = 10; // maxmum finite relaxation time
const B0max = 6;
var savedState = {}; // used to save common parameters.
var savedState2 = []; // used to save isochromate's parameters.
var savedFlag = false; // is restoring of state an option?
var paused = false;
var guiFolderStrs = []; //stack of folder-names
var guiFolders = []; //stack of folders
var addSampleFolder = false; //menu item for samples may confuse, so hidden.
var nFolder = -1;
var updateMenuList = [];
var guiViewsFolder; // index of Fields menu item.
var guiFieldsFolder; // index of Fields menu item.
var guiGradientsFolder; // index of Gradient menu item.
var guiFolderFlags = [true, true, true, true, true, true, true, true, true]; //folder closed?
const guiUpdateInterval = 0.1; // seconds
var guiTimeSinceUpdate = 0;
const nullvec = new THREE.Vector3(0., 0., 0.);
const unitXvec = new THREE.Vector3(1., 0., 0.);
const unitYvec = new THREE.Vector3(0., 1., 0.);
const unitZvec = new THREE.Vector3(0., 0., 1.);
var curveBlue = [], curveBlueTimes = [];
var curveGreen = [], curveGreenTimes = [];
var MxCurve = [], MxTimes = [];
var MxyCurve = [], MxyTimes = [];
var MzCurve = [], MzTimes =[];
var MxLabelIdent = $( '#MxLabel');
var MxyLabelIdent = $( '#MxyLabel');
var MzLabelIdent = $( '#MzLabel');
var FIDcanvas = document.getElementById("FIDcanvas");
var FIDcanvasAxis = document.getElementById("FIDcanvasAxis");
var FIDctx = FIDcanvas.getContext("2d");
var FIDctxAxis = FIDcanvasAxis.getContext("2d");
var grWidth = FIDcanvas.width;
FIDcanvas.height = grWidth; // Needed to get aspect ratio of voxels right.
FIDcanvasAxis.height = grWidth; // Apparantly canvas has two heights (clientheight)
var grHeight = FIDcanvas.height;
const FIDduration = 4000; //ms
var trigSampleChange = false;
var lastB1freq = 0;
var dtTotal=0, dtCount=0;
// var dtMemory = Array(10).fill(0), dtMemIndi = 0; // not IE compat
var dtMemory = [0,0,0,0,0,0,0,0,0,0], dtMemIndi = 0;
var spoilR2 = 0;
var scenes = []; // Array of functions defining scenes.
var floor, floorRect, floorCirc;
var B1cyl, B1shadow;
var frameFixed = false; //Frame is fixed when spatial axis is active.
var framePhase = 0; //phase of rotating frame
var framePhase0 = 0; //phase of rotating frame at start of RF pulse.
var statsContainer, stats;
const torqueScale = 0.5;
const gradScale = 11;
const B1scale = 0.4;
const spoilDuration = 1000;
var spoilTimer1, spoilTimer2, spoilTimer3;
var restartRepIfSampleChange = false;
var restartRepIfSampleChangeTimer;
var exciteTimers = [];
const white = new THREE.Color( 'white' );
const greenStr = 'lawngreen';
const green = new THREE.Color( greenStr ); //only this can be chosen freely.
const red = new THREE.Color( 'red' );
const blueStr = 'dodgerblue';
const blue = new THREE.Color( blueStr );
const nZeroSinc = 4; // 4 for 3-lobe sinc. Matching 0.22571 appears below.
// A sinc with same B1 and duration as rect has durCorrSinc area ratio.
// This is compensated by prolonging sinc pulse below. Wolfram knows Si(x).
const durCorrSinc = 0.22571; // Si(2 pi)/(2 pi). Generally Si(nZ/2 pi) / (nZ/2 pi).
const radius = 0.03; //cylinder radius
const myShadow = true; // Shadows drawn manually.
var shadowMaterial = new THREE.MeshBasicMaterial({ color: 0x808070});
var shadowMaterials = [];
const downViewThresh = Math.PI/4; // shadow decreases below 45 degree polar.
// Semi-transparant test:
// var shadowMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.5, blending: THREE.NormalBlending});
var torqueMaterial = new THREE.MeshLambertMaterial({ color: 0xc80076 });
var B1effMaterial = new THREE.MeshLambertMaterial({ color: "blue" });
var floorMaterial = new THREE.MeshLambertMaterial({ color: 0xb0b090 }); //s:0x808070
// var floorMaterialFixed = new THREE.MeshLambertMaterial({ color: 0x90b0a0 });
var floorMaterialFixed = new THREE.MeshLambertMaterial({ color: 0x90b0d0 });
var floorMaterialBlack = new THREE.MeshLambertMaterial({ color: 0x303030 });
const nShadowColors = 16;
var doStats = false; // framerate statistics
var addAxisHelper = false;
var threeShadow = false; // Let Three.js handle shadows.
// x-rotation 90 for testing:
var propagator90x = new THREE.Matrix4().set(
1, 0, 0, 0,
0, Math.cos(Math.PI/2), Math.sin(Math.PI/2), 0,
0, -Math.sin(Math.PI/2), Math.cos(Math.PI/2), 0,
0, 0, 0, 1);
var propagator90y = new THREE.Matrix4().set(
Math.cos(Math.PI/2), 0, Math.sin(Math.PI/2), 0,
0, 1, 0, 0,
-Math.sin(Math.PI/2), 0, Math.cos(Math.PI/2), 0,
0, 0, 0, 1);
function cylinderMesh(fromVec, toVec, material, nElem, radius) {
var vec = toVec.clone().sub(fromVec);
var h = vec.length();
vec.divideScalar(h || 1);
var quaternion = new THREE.Quaternion();
quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), vec);
var geometry = new THREE.CylinderBufferGeometry(radius, radius, h, nElem);
// BufferGeometries (eg. CylinderBufferGeometry) are faster
// than Geometries and require less memory. See
// https://threejsfundamentals.org/threejs/lessons/threejs-custom-buffergeometry.html
geometry.translate(0, h/2, 0);
var cylinder = new THREE.Mesh(geometry, material);
cylinder.applyQuaternion(quaternion);
cylinder.position.set(fromVec.x, fromVec.y, fromVec.z);
return cylinder;
} //cylinderMesh
function shadowMesh(Mvec) {
var MvecTrans = new THREE.Vector2(Mvec.x, Mvec.y);
var MvecTransLength = MvecTrans.length();
var direction = Mvec.clone().projectOnPlane(unitZvec);
var orientation = new THREE.Matrix4();
orientation.lookAt(nullvec, Mvec.projectOnPlane(unitZvec), new THREE.Object3D().up);
orientation.multiply(new THREE.Matrix4().set( 1, 0, 0, 0,
0, 0, 1, 0,
0, -1, 0, 0,
0, 0, 0, 1));
var shadowBarGeo = new THREE.PlaneGeometry(2*radius, MvecTransLength);
shadowBarGeo.translate( 0, MvecTransLength/2, -1.1);
var shadowBarMesh = new THREE.Mesh(shadowBarGeo);
var shadowEndGeo = new THREE.CircleGeometry(radius, 4, 0, Math.PI); //only dot at far end.
shadowEndGeo.translate( 0, MvecTransLength, -1.1);
var shadowEndMesh = new THREE.Mesh(shadowEndGeo);
shadowEndMesh.rotation.z = MvecTrans.angle()+Math.PI/2;
var shadowGeo = new THREE.Geometry(); //merged BufferGeometry shadows fail to render with no
// messages issued. Added Buffer here and above. https://stackoverflow.com/questions/36450612
shadowGeo.merge(shadowBarMesh.geometry, shadowBarMesh.matrix);
shadowGeo.merge(shadowEndMesh.geometry, shadowEndMesh.matrix);
var mesh = new THREE.Mesh(shadowGeo, shadowMaterial);
mesh.applyMatrix4(orientation);
return mesh;
} //shadowMesh
function Isoc(M, color, pos, nElem, showCurve, dR1, dR2, M0, dRadius) { //isocromate constructor
this.M = M.clone();
this.dB0 = 0; //spatially independent field offset
this.detuning = 0; // total field offset from RF freq
this.dMRF = new THREE.Vector3(0, 0, 1);
this.color = color; // don't clone since color-pointer is used as identifier.
this.pos = pos.clone();
this.showCurve = showCurve;
this.dR1 = dR1;
this.dR2 = dR2;
this.M0 = (M0 >= 0) ? M0 : 1;
this.dRadius = dRadius?dRadius: 0;
nElem || (nElem = 8); // controls cylinder surface smoothness.TODO: reduce for small screen.
var cylMaterial = new THREE.MeshLambertMaterial({ color: color });
this.cylMesh = cylinderMesh(new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 1, 0),
cylMaterial, nElem, radius + this.dRadius);
scene.add(this.cylMesh);
this.torque = cylinderMesh(new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 1, 0),
torqueMaterial, nElem, radius + this.dRadius);
scene.add(this.torque);
this.B1eff = cylinderMesh(new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 1, 0),
B1effMaterial, nElem, 1.01*(radius + this.dRadius));
scene.add(this.B1eff);
if (myShadow) {
// Shadows are initialized along y to make length right subsequently.
this.shadow = shadowMesh(new THREE.Vector3(0, 1, 0));
scene.add(this.shadow);
this.tshadow = shadowMesh(new THREE.Vector3(0, 1, 0));
scene.add(this.tshadow);
}
} //Isoc
// Use prototype to avoid copies for each instance.
// Likely more manipulation should be in prototype.
// https://medium.com/better-programming/prototypes-in-javascript-5bba2990e04b
Isoc.prototype.scale = function (scalar) { //adds method "scale" to Isoc prototype.
this.M = this.M.multiplyScalar(scalar);
this.dMRF = this.dMRF.multiplyScalar(scalar);
return this;
}
Isoc.prototype.remove = function () {
scene.remove(this.cylMesh);
scene.remove(this.torque);
scene.remove(this.B1eff);
if (myShadow) {
scene.remove(this.shadow);
scene.remove(this.tshadow);
}
}
function scaleIsocArr(isocArr, factor) {
isocArr.forEach(function (item, index) {item.scale(factor)});
return isocArr;
}
function removeIsocArr() {
state.IsocArr.forEach(function (item, index) {item.remove()});
}
function guiAddFolder(StrClosed,StrOpen,AdderFct,cFolder,createFromFolder) {
if (createFromFolder <= 0) { // does nothing unless index to start creating from is zero (or less)
let folderLabel = guiFolderFlags[cFolder]?StrClosed:StrOpen;
guiFolderStrs.push(folderLabel);
let guiFolder = gui.addFolder(folderLabel);
guiFolders.push(guiFolder);
AdderFct(guiFolder);
if (createFromFolder < 0) {
guiFolderFlags[cFolder]=true; }
if (guiFolderFlags[cFolder]) {
guiFolder.close();
// last folder's state of openess decides viewing order of stacked elems:
if (cFolder == nFolder) //reset depth to normal if last folder is closed.
eventButtons.css('z-index',20);
}
else {
guiFolder.open();
if (cFolder == nFolder-1) //lower buttons if last folder is open.
eventButtons.css('z-index',0); // for small displays.
}
return guiFolder;
}
} //guiAddFolder
function guiCloseFolder (guiFolder) {
// possibly useful for Help dat-gui-item that does not need label updating.
guiFolder.close();
guiFolderFlags[ guiFolders.indexOf(guiFolder) ] = true;
} //guiCloseFolder
function scaleMultiple(magVecs, scalar){
for(let i = 0; i < magVecs.length; i++)
magVecs[i].scale(scalar);
}
function RFconst(B1, B1freq) {
let phase = B1freq * state.tSinceRF - state.phi1 + framePhase0;
return [new THREE.Vector3(B1*Math.cos(phase), -B1*Math.sin(phase), 0.), B1];
}
function RFsincWrapper (duration) { //wrapper needed to avoid recalc of duration.
return function RFsinc(B1, B1freq) {
let phase = B1freq * state.tSinceRF - state.phi1 + framePhase0;
let sincArg = nZeroSinc * Math.PI * (state.tSinceRF/duration-1/2);
let envelope = (Math.abs(sincArg) > 0.01) ?
(B1*Math.sin(sincArg)/sincArg) : B1;
return [new THREE.Vector3(envelope*Math.cos(phase),
-envelope*Math.sin(phase), 0.),
envelope];
}
}
function RFpulse(type, angle, phase, B1) {
let gamma = state.Gamma;
state.tSinceRF = 0; // Both area and time left is needed for pulse with
state.areaLeftRF = angle; // sidelobes. Area is adjusted at temporal end.
let duration = angle / (gamma*B1);
state.B1 = B1;
state.B1freq = gamma * state.B0;
var dtAvg = dtMemory.reduce( function (a,b) {return a + b}, 0) / dtMemory.length; //short function notation is not IE compatible.
phase += state.B1freq * gamma * dtAvg/2; //small phase correction
framePhase0 = framePhase;
state.phi1 = phase;
switch (type) {
case 'rect': state.RFfunc = RFconst; break;
case 'sinc': duration = duration / durCorrSinc;
state.RFfunc = RFsincWrapper(duration); break;
default: alert('Unknown RF pulse type');
}
state.tLeftRF = duration;
updateMenuList.push(guiFieldsFolder); //mark field folder for updating
} //RFpulse
function spoil() {
spoilR2 = 4.7;
window.setTimeout(
function () {
spoilR2 = 0;
if (state.Sample == "Thermal ensemble") return; //dont spoil thermal.
for (var i = 0; i < state.IsocArr.length; i++) { //kill any remaining Mxy.
state.IsocArr[i].M.projectOnVector(unitZvec);}},
spoilDuration); //ms
} //spoil
function gradPulse(phaseDiff, directionAngle) {
const gradDur = 1; //s
state.areaLeftGrad = phaseDiff*gradScale/state.Gamma;
if (directionAngle) {
state.Gx = Math.cos(directionAngle) * state.areaLeftGrad/gradDur;
state.Gy = Math.sin(directionAngle) * state.areaLeftGrad/gradDur;
} else { //default is Gx
state.Gx = state.areaLeftGrad/gradDur;
directionAngle = 0;
}
state.PulseGradDirection = directionAngle;
updateMenuList.push(guiGradientsFolder);
} // gradPulse
function gradRefocus() {
let isocArr = state.IsocArr;
let meanPhaseDiff = 0;
let MxyLeft, MxyRight;
let dx,weight;
let totalWeight = 0;
let phaseRight; // Right isocs phase
let phaseDiff; //phase difference
MxyLeft = isocArr[0].M.clone().projectOnPlane(unitZvec);
for(let i = 1; i <= isocArr.length-2; i++) {
dx = isocArr[i].pos.x - isocArr[i-1].pos.x;
if (dx > 0) { // ignore lineshifts in plane
MxyRight = isocArr[i].M.clone().projectOnPlane(unitZvec);
weight = Math.min(MxyLeft.length(), MxyRight.length());
totalWeight += weight;
phaseRight = Math.atan2(MxyRight.y, MxyRight.x);
MxyLeft.applyAxisAngle(unitZvec,-phaseRight); //rotate left isoc by right's angle.
phaseDiff = Math.atan2(MxyLeft.y, MxyLeft.x);
meanPhaseDiff += weight * phaseDiff / dx;
MxyLeft = MxyRight; // right is the new left
}
}
if ((Math.abs(meanPhaseDiff)>0.001) && (totalWeight>0.01)) {
meanPhaseDiff = meanPhaseDiff/totalWeight;
gradPulse(-meanPhaseDiff);
}
} // gradRefocus
function thermalDrawFromLinearDist(B0) { //draws sample from -1 to 1
const pol = B0/B0max; // 0 to 1. Zero gives uniform distribution.
var sample;
let random = Math.random();
if (random > pol) //sample from uniform dist, if random is gt pol-treshold.
sample = 2*Math.random()-1
else
sample = 2*Math.sqrt(Math.random())-1; //linear dist
return sample;
}
function magInit() {
const c10 = Math.cos(10*Math.PI/180);
const s10 = Math.sin(10*Math.PI/180);
const c30 = Math.cos(30*Math.PI/180);
const s30 = Math.sin(30*Math.PI/180);
const eps = 0.05;
const xz30 = new THREE.Vector3(s30, 0, c30);
const x = new THREE.Vector3(1, 0, 0);
const y = new THREE.Vector3(0, 1, 0);
const z = new THREE.Vector3(0, 0, 1);
const nx = new THREE.Vector3(-1, 0, 0);
const ny = new THREE.Vector3(0, -1, 0);
const nz = new THREE.Vector3(0, 0, -1);
const xyz = new THREE.Vector3(1 + eps, 1 - eps, 1);
const xynz = new THREE.Vector3(1, 1, -1);
const xnyz = new THREE.Vector3(1 + eps, -1 + eps, 1);
const nxyz = new THREE.Vector3(-1 - eps, 1 - eps, 1);
const nxnyz = new THREE.Vector3(-1 - eps, -1 + eps, 1);
const xnynz = new THREE.Vector3(1, -1, -1);
const nxynz = new THREE.Vector3(-1, 1, -1);
const nxnynz = new THREE.Vector3(-1, -1, -1);
function IsocXz30() {return new Isoc(xz30, // M
white, // color
nullvec); } // pos
function IsocX() { return new Isoc(x, white, nullvec); }
function IsocY() { return new Isoc(y, white, nullvec); }
function IsocZ() { return new Isoc(z, white, nullvec); }
function IsocZensembleRed() { return new Isoc(z, red, nullvec); }
let nElem = 8; //controls cylinder surface smoothness.
function IsocZgreen() { let M0 = 0.91;
return new Isoc(
new THREE.Vector3(0, 0, M0),
green, nullvec, nElem, // Note: added relax dR1, dR2 must be pos.
true, 0, 0, M0, 0); } //showCurve, dR1, dR2, M0, dRadius
function IsocZblue() { let M0 = 1.0;
return new Isoc(
new THREE.Vector3(0, 0, M0),
blue, nullvec, nElem,
true, 0.2, 0.2, M0, 0.001); } //showCurve, dR1, dR2, M0, dRadius
function IsocZwhite() {let M0 = 0.91;
return new Isoc(
new THREE.Vector3(0, 0, M0),
white, nullvec, nElem,
true, 0, 0.2, M0, 0.0008); } //showCurve, dR1, dR2, M0, dRadius
function IsocNX() { return new Isoc(nx, white, nullvec); }
function IsocNY() { return new Isoc(ny, white, nullvec); }
function IsocNZ() { return new Isoc(nz, white, nullvec); }
function IsocXYZ() { return new Isoc(xyz, white, nullvec); }
function IsocXYNZ() { return new Isoc(xynz, white, nullvec); }
function IsocXNYZ() { return new Isoc(xnyz, white, nullvec); }
function IsocNXYZ() { return new Isoc(nxyz, white, nullvec); }
function IsocNXNYZ() { return new Isoc(nxnyz, white, nullvec); }
function IsocXNYNZ() { return new Isoc(xnynz, white, nullvec); }
function IsocNXYNZ() { return new Isoc(nxynz, white, nullvec); }
function IsocNXNYNZ() { return new Isoc(nxnynz, white, nullvec); }
let basicState= {IsocArr:[], B1:0.0, Gamma:1,
B0:2.,
// t:0, //removed to avoid resetting of FID. Seems ok.
dt:0.01, phi1:0., T1:Infinity, T2:Infinity, B1freq: 5,
Name:'', RFfunc:RFconst};
scenes.Isoc1 = function () {
return {IsocArr:[IsocZ()]}};
scenes.Precession = function () {
Object.assign(state, basicState);
return {IsocArr:[IsocXz30()]}};
scenes.Equilibrium = function ()
{ Object.assign(state, basicState);
return {IsocArr:[IsocZ()],
T1: Infinity, T2: Infinity,
RFfunc:RFconst,
viewMx:true}; }
scenes.IsocInhomN = function (nIsoc) {
let inhom = {IsocArr:[]};
const spreadScale = 1/6;
const nonlinScale = Math.PI/1.5; //reduces recovery
for(let i=0; i<nIsoc; i++) {
inhom.IsocArr.push(IsocZ());
inhom.IsocArr[i].dB0 =
Math.tan( (i-(nIsoc-1)/2) / (nIsoc/nonlinScale) ) * spreadScale;
}
return inhom;
} //IsocInhomN
scenes.Inhomogeneity = function () {
Object.assign(state, basicState);
Object.assign(state, scenes.IsocInhomN(9));
return {};
}
scenes.ThermalEnsemble = function () {
// Creates pseudo random state that appears more random than random.
// For each cosTheta, 3+-k*B0 isocs evenly rotated over cirle are added.
let B0 = B0max;
const nBand = 100; //select even
const perBand = 3;
var Isocs = [];
var cosTheta;
let M = new THREE.Vector3;
for(let i=0; i < nBand; i++) {
cosTheta = i - nBand/2 + 0.5; //symmetric and avoid extremes
cosTheta = cosTheta / (nBand/2); // -1 < cosTheta < 1
let phi = Math.random() * 2 * Math.PI;
let perBandAdjusted = Math.round((1+cosTheta*(B0/B0max))*perBand);
for (let j=0; j < perBandAdjusted ; j++) {
M.z = cosTheta;
let Mxy = Math.sqrt(1 - M.z * M.z);
let arg = phi + (2*Math.PI) * (j + Math.random()/2) / perBandAdjusted;
M.x = Mxy * Math.cos(arg);
M.y = Mxy * Math.sin(arg);
Isocs.push( new Isoc(M, white, nullvec) );
}
}
return {IsocArr:Isocs, viewMz:true, // B0:B0, (left low for better viz)
FrameStat:false, FrameB0:true, FrameB1:false};
} //ThermalEnsemble
scenes.ThermalEnsembleSimple = function () { // NOT used currently.
// simplified view. Fails when relaxation is added.
let axisVecs = [IsocZensembleRed().scale(1.03), IsocNZ().scale(0.97),
IsocX(), IsocY(), IsocNX(), IsocNY()];
axisVecs[0].color = red;
let diagVecs = [IsocXYZ(), IsocNXYZ(), IsocXNYZ(), IsocXYNZ(),
IsocNXNYZ(), IsocNXYNZ(), IsocXNYNZ(), IsocNXNYNZ()];
diagVecs = scaleIsocArr(diagVecs, 1/Math.sqrt(3));
return {IsocArr: axisVecs.concat(diagVecs) };
} //ThermalEnsembleSimple
scenes.Ensemble = function () {
Object.assign(state, basicState);
return scenes.ThermalEnsemble();
}
scenes.Substances3 = function () {
let isocs = {IsocArr: [IsocZblue(), IsocZgreen(), IsocZwhite()]};
isocs.IsocArr[0].dB0 = 0;
isocs.IsocArr[1].dB0 = -0.04;
isocs.IsocArr[2].dB0 = 0.04;
return isocs;
}
scenes.MixedMatter = function () {
Object.assign(state, basicState);
state.T1 = 8;
state.T2 = 5;
Object.assign(state, scenes.Substances3());
return {viewMx: false, viewMxy: true};
}
scenes.Line = function () {
const nIsoc = 21; // choose odd number of isochromates.
var line = [];
for(let i=0; i<nIsoc; i++) {
line.push(IsocZ());
line[i].pos.setX((i-(nIsoc-1)/2)*0.4);
}
return {IsocArr: line, allScale: 0.35, Gx: 3};
}
// scenes.LineDense = function () {
// const nIsoc = 41; // choose odd number of isochromates.
// var line = [];
// for(let i=0; i<nIsoc; i++) {
// line.push(IsocZ());
// line[i].pos.setX((i-(nIsoc-1)/2)*0.2);
// }
// return {IsocArr: line, allScale: 0.35};
// }
scenes.LineDense = function (uniform) {
const nIsoc = 41; // Not all are realized for structured object.
var line = [];
var shift = 1;
for(let i=0; i<nIsoc; i++) {
if (uniform || ((Math.floor((i-shift)/3) % 2) == 0)) {
let isoc = IsocZ();
isoc.pos.setX((i-(nIsoc-1)/2)*0.2);
line.push(isoc);
}
}
return {IsocArr: line, allScale: 0.35, Gx: -6};
}
scenes.Plane = function () {
const nIsoc = 21; // choose odd number of isochromates.
var plane = [];
for(let i=0; i<nIsoc; i++) {
for(let j=0; j<nIsoc; j++) {
plane.push(IsocZ());
plane[i*nIsoc+j].pos.set((j-(nIsoc-1)/2)*0.4,(i-(nIsoc-1)/2)*0.4,0);
}
}
return {IsocArr: plane, allScale: 0.35};
}
scenes.WeakGradient = function () {
Object.assign(state, basicState);
Object.assign(state, scenes.Line());
return {B1freq: 3};
}
scenes.StrongGradient = function (uniform) {
Object.assign(state, basicState);
Object.assign(state, scenes.LineDense(uniform));
return {B1freq: 0, FrameB0:true, FrameB1:false, FrameStat:false};
}
} //magInit
function initFIDctxAxis() {
FIDctxAxis.clearRect(0, 0, grWidth, grHeight);
FIDctxAxis.save();
FIDctxAxis.strokeStyle = 'gray';
FIDctxAxis.fillStyle = 'gray';
FIDctxAxis.lineWidth = 1;
let offset = 4; //half triangle size
FIDctx.translate(offset, grHeight/2);
FIDctx.scale(0.95,0.95);
FIDctx.translate(offset, -grHeight/2);
FIDctxAxis.beginPath();
FIDctxAxis.moveTo(offset, 0); //vertical axis:
FIDctxAxis.lineTo(offset, grHeight);
let nTick = 8; // tick marks:
for(let cTick=1; cTick<nTick; cTick++) {
FIDctxAxis.moveTo(-offset, grHeight*cTick/nTick);
FIDctxAxis.lineTo(offset, grHeight*cTick/nTick);
}
FIDctxAxis.stroke();
FIDctxAxis.beginPath(); // triangle:
FIDctxAxis.moveTo(offset, 0);
FIDctxAxis.lineTo(0, 2*offset);
FIDctxAxis.lineTo(2*offset, 2*offset);
FIDctxAxis.fill();
FIDctxAxis.beginPath();
FIDctxAxis.moveTo(offset, grHeight/2); //horizontal axis:
FIDctxAxis.lineTo(grWidth, grHeight/2);
FIDctxAxis.stroke();
FIDctxAxis.beginPath(); // triangle:
FIDctxAxis.moveTo(grWidth-2*offset, grHeight/2-offset);
FIDctxAxis.lineTo(grWidth, grHeight/2);
FIDctxAxis.lineTo(grWidth-6, grHeight/2+offset);
FIDctxAxis.fill();
FIDctxAxis.restore();
} //initFIDctxAxis
function sampleChange() {
if (paused) {
paused = false;
$( "#Pause" ).button( "option", "label", "||");
}
trigSampleChange = false; //clear request for further updating.
removeIsocArr();
state.allScale = 1; //default
state.curveScale = 1; //default
switch (state.Sample) {
case 'Precession': /* Scene-changes from here: */
state = Object.assign(state, scenes.Precession());
state.Sample = '1 isochromate';
frameFixed = false; //TODO: Why is frameFixed not in scenes-definitions?
$("#Presets").css('color', '#bbbbbb'); break;
case 'Equilibrium':
state = Object.assign(state, scenes.Equilibrium());
state.Sample = '1 isochromate';
frameFixed = false;
$("#Presets").css('color', '#ffffff'); break;
case 'Inhomogeneity':
state = Object.assign(state, scenes.Inhomogeneity());
state.Sample = '9 isochromates';
frameFixed = false;
$("#Presets").css('color', '#ffffff'); break;
case 'Ensemble':
state = Object.assign(state, scenes.Ensemble());
state.Sample = 'Thermal ensemble';
state.curveScale = 2;
frameFixed = false;
$("#Presets").css('color', '#ffffff'); break;
case 'Weak gradient':
state = Object.assign(state, scenes.WeakGradient());
frameFixed = true;
state.Sample = 'Line';
$("#Presets").css('color', '#ffffff'); break;
case 'Strong gradient':
state = Object.assign(state, scenes.StrongGradient(true));
frameFixed = true;
state.Sample = 'Line, dense';
$("#Presets").css('color', '#ffffff'); break;
case 'Structure':
state = Object.assign(state, scenes.StrongGradient(false));
frameFixed = true;
state.Sample = 'Line, structured';
$("#Presets").css('color', '#ffffff'); break;
case 'Mixed matter':
state = Object.assign(state, scenes.MixedMatter());
state.Sample = '3 substances';
frameFixed = false;
$("#Presets").css('color', '#ffffff'); break; /* Sample-changes from here: */
case '1 isochromate':
state = Object.assign(state, scenes.Isoc1());
frameFixed = false; break;
case '9 isochromates':
state = Object.assign(state, scenes.IsocInhomN(9));
frameFixed = false; break;
case '3 substances':
state = Object.assign(state, scenes.Substances3());
frameFixed = false; break;
case 'Thermal ensemble':
state = Object.assign(state, scenes.ThermalEnsemble());
frameFixed = false; break;
case 'Line':
state = Object.assign(state, scenes.Line());
frameFixed = true; break;
case 'Line, dense':
state = Object.assign(state, scenes.LineDense(true));
frameFixed = true; break;
case 'Line, structured':
state = Object.assign(state, scenes.LineDense(false));
frameFixed = true; break;
case 'Plane':
state = Object.assign(state, scenes.Plane());
frameFixed = true; break;
default: alert("Sample changed to "+state.Sample);
}
if (restartRepIfSampleChange) { // Re-excite is relevant for multi-SE.
clearRepTimers();
restartRepIfSampleChangeTimer = window.setTimeout(
function() {
let elem = document.getElementById('RepExc');
let label = elem.textContent || elem.innerText || "";
buttonAction(label);
}
, 4000);
}
if (frameFixed && (! state.FrameStat))
floor.material = floorMaterialFixed;
else
floor.material = floorMaterial;
shadowMaterialsInit(floor.material);
// Update FID label visibility:
state.viewMx ? MxLabelIdent.show() : MxLabelIdent.hide();
state.viewMz ? MzLabelIdent.show() : MzLabelIdent.hide();
state.viewMxy ? MxyLabelIdent.show() : MxyLabelIdent.hide();
curveBlue.forEach(function (item, index) { curveBlue[index] = 0 });
curveGreen.forEach(function (item, index) { curveGreen[index] = 0 });
} //sampleChange
function guiInit(removeFolderArg) {
// Initializes new gui, or removes&recreates from folder removeFolderArg for updating.
// There may be alternatives for updating dat-gui, but this was made before knowing, e.g. so
// gui.__folders['Relaxation: Off'].__controllers[1].setValue(Infinity));
// For details and helper functions see:
// https://stackoverflow.com/questions/16166440/refresh-dat-gui-with-new-values
// My solution may be somewhat slow, and seems to prevent dat-gui presets from working well.
var createFromFolder;
debug && console.log('guiInit called. Argument: ' + (removeFolderArg?removeFolderArg:''));
if (!gui) { // if new gui
blochMenu = {
GetStarted: dialog("dialogGetStarted"),
VideoIntros: dialog("dialogVideoIntros"),
GetApps: dialog("dialogGetApps"),
Tools: dialog("dialogTools"),
About: dialog("dialogAbout"),
Reset: function () { trigSampleChange = true; }
}
state = { //dummy example values. More are added for some samples.
B0: 0,
B1: 0,
B1freq: 0, //angular frequency
phi1: 0, //RF phase in rotating frame.