-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathdev.html
More file actions
986 lines (934 loc) · 65.3 KB
/
Copy pathdev.html
File metadata and controls
986 lines (934 loc) · 65.3 KB
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
<!--
index.html
----------
AI Context: STANDALONE WEB APP ENTRY POINT
- The main HTML file for the standalone web application.
- Loads every source file directly, in the order tools/bundle.py declares.
A deployed page can load one bundle instead; see tools/bundle.py BUNDLES.
- Defines the DOM structure for the viewer, sidebar, and controls.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2Dmol</title>
<!-- No Tailwind. It was loaded from cdn.tailwindcss.com - an in-browser JIT
compiler that re-scans the DOM on every mutation, and warns in the
console that it is not for production - to supply EIGHT utility rules.
They are in src/app/style.css now, with Tailwind's own values. -->
<!-- JSZip library for handling zip file uploads -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<!-- canvas2svg is src/core/svg.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
xintegrity="sha512-SnH5WK+bZxgPHs44uWIX+LLMDJd4gYm4zQ6UvE6j3v/3Tf7U7T2Y9p6gJzU0bW5V6f6G6X6b6W6w6+g6w=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="src/app/style.css">
</head>
<body class="bg-gray-50 text-gray-800 font-sans p-4">
<!-- Global Drop Overlay Element -->
<div id="global-drop-overlay">
<p><i class="fa-solid fa-cloud-arrow-up mr-3"></i> Drop File Anywhere to Load</p>
</div>
<!-- End Global Drop Overlay -->
<!-- Total width (600 + 8 + 340 = 948px) -->
<div class="mx-auto" style="width: 948px; position: relative;">
<!-- SAVE, CLEAR ALL AND GPU are about the page, not about the thing you
are loading, and they are here every second of a session while
Fetch and Upload are used once. Smaller and quieter than the
controls in the panel below for that reason: three full-size
buttons in strong colours at the top of the page took the eye
first, and the first thing to do on this page is load something. -->
<div class="page-actions"
style="position: absolute; top: 0; right: 0; display: flex; gap: 6px; align-items: center;">
<!-- USE GPU sits with Save and Clear All because it is a GLOBAL setting,
not something about the file being fetched. It is the rendering
BACKEND and applies to both styles equally, and it wears the same
skin as the toggles in the Style panel - green on, grey off - at the
size of the two buttons beside it.
ON BY DEFAULT, on this page only. Measured first render of 3J3Q:
1,813 ms on the 2D path against 455 with the GPU, and every frame
after it 840 ms against 26 - turning a capsid is not usable
without it. Small structures are unaffected either way (1TIM 306
vs 327 ms, 4UG0 332 vs 333).
Safe as a default HERE because this page is the one build that
keeps both painters: the control removes itself when WebGL2 is
absent, and the renderer falls back to the 2D path for anything
the GPU declines. Nowhere else has that fallback - the notebook
and embed bundles ship the WebGL2 painter alone, and their
default is gpu=True with nothing behind it. -->
<label id="useGpuRow" class="btn-toggle-global"
title="Draw on the GPU (WebGL2): the same picture, but turning and zooming cost one draw call instead of a full repaint. Applies to both styles.">
<input type="checkbox" id="useGpuCheckbox" checked>
<span><i class="fa-solid fa-microchip"></i>GPU</span>
</label>
<button id="saveStateButton" class="btn btn-primary">
<i class="fa-solid fa-floppy-disk"></i>
<span>Save</span>
</button>
<button id="clearAllButton" class="btn btn-danger">
<i class="fa-solid fa-trash"></i>
<span>Clear All</span>
</button>
</div>
<!-- JUST THE TITLE. The line under it said "Upload a PDB/CIF file, or
fetch by PDB or UniProt ID", which is a description of the two
controls directly beneath it: a box whose placeholder is an ID and
a button that says Upload. The chain suffixes it used to carry -
the only part of the syntax nobody can guess - are in that box's
placeholder now (1a3n_AB), where someone about to type an ID is
already looking. -->
<div class="page-head">
<h1>2Dmol</h1>
</div>
<div id="upload-options-container" class="container-box" style="width: 948px; margin-bottom: 10px;">
<!-- FETCH PANEL. Three lines, and the third is usually not there.
It was a two-column table: an ID box and a Load button on the
left, seven toggles standing open on the right, and the
examples squeezed into half a column where four of them
wrapped onto two rows. Everything about that layout was sized
by the options, which are the part almost nobody changes -
they are defaults, and a default that is right does not need
to be on screen. So they fold behind a button and the row that
matters gets the whole width. -->
<div class="fetch-panel">
<!-- WHAT TO LOAD, all on one line. The label the ID box used to
carry is the placeholder now: it said the same words, in
the same place, one line further up. -->
<div class="fetch-row" id="fetch-input-container">
<input type="text" id="fetch-id"
aria-label="PDB ID (4 characters, optionally +chains) or UniProt ID"
placeholder="e.g., 1A3N, 1a3n_AB or P0A8I3">
<button id="fetch-btn" class="btn btn-primary">Fetch</button>
<button id="upload-button" class="btn btn-secondary">
<i class="fa-solid fa-file-arrow-up"></i>
<span>Upload</span>
</button>
<input type="file" id="file-upload"
accept=".pdb,.cif,.ent,.zip,.json,.cst,.a3m,.fasta,.fa,.fas,.sto,.csv" multiple
class="hidden">
<!-- aria-expanded and aria-controls, because a button that
shows something has to say so to anything not looking
at it. -->
<button id="fetchOptionsButton" class="btn btn-grey" type="button"
aria-expanded="false" aria-controls="fetchOptions"
title="Loading options: frames, alignment, PAE, biounit, ligands">
<span>Options</span>
<span id="fetchOptionsCaret" aria-hidden="true">▾</span>
</button>
</div>
<!-- ONE ROW, which is what the whole width buys. Wired
generically off data-example-value, so adding one needs no
JS - but the row does not wrap any more, so adding a fifth
is now a decision about width rather than a free action. -->
<div class="fetch-row fetch-examples">
<span class="fetch-row-label">Examples</span>
<button class="btn btn-success btn-small" data-example-value="6MRR">
<span>6MRR</span>
</button>
<button class="btn btn-success btn-small" data-example-value="Q5VSL9">
<span>Q5VSL9</span>
</button>
<button class="btn btn-success btn-small" data-example-value="1YNE">
<span>1YNE</span>
</button>
<button class="btn btn-success btn-small" data-example-value="1A3N">
<span>1A3N</span>
</button>
</div>
<!-- ...AND THE OPTIONS, folded away, IN GROUPS. Eight
switches in one flowing row is a list to read rather than
a thing to scan: they answer three different questions,
and which one a switch belongs to is not obvious from its
name (Align chain is about frames, Filter Additives is
about what the file contains). Grouped, each column is one
question, and the two columns you are not asking can be
skipped in a glance. -->
<div class="fetch-options" id="fetchOptions" hidden>
<!-- MANY MODELS IN ONE FILE, and what to do with them. -->
<div class="fetch-option-group">
<span class="fetch-option-title">Frames</span>
<div class="upload-toggle-item">
<label class="switch">
<input type="checkbox" id="loadAsFramesCheckbox" checked>
<span class="slider-toggle"></span>
</label>
<label for="loadAsFramesCheckbox" class="upload-toggle-label">Load as Frames</label>
</div>
<div class="upload-toggle-item">
<label class="switch">
<input type="checkbox" id="alignFramesCheckbox" checked>
<span class="slider-toggle"></span>
</label>
<label for="alignFramesCheckbox" class="upload-toggle-label">Align Frames</label>
</div>
<!-- WHICH CHAIN THE FRAMES ARE ALIGNED ON. Blank keeps
the old behaviour - the first chain in the reference
frame - which is right until the first chain is not
the one you care about. Only read when Align Frames
is on, which is why it sits under it. -->
<div class="upload-toggle-item" id="alignChainRow">
<label for="alignChainInput" class="upload-toggle-label"
title="Align the frames on this chain. Blank uses the first chain in the structure.">Align chain</label>
<input type="text" id="alignChainInput" placeholder="auto"
maxlength="4" autocomplete="off" spellcheck="false"
style="width: 64px; height: 26px; font-size: 12px; padding: 0 6px; border: 1px solid #d1d5db; border-radius: 6px;">
</div>
</div>
<!-- WHAT OF THE FILE ENDS UP IN THE PICTURE. All three
change which atoms are there at all, which is a
different question from how they are drawn. -->
<div class="fetch-option-group">
<span class="fetch-option-title">Structure</span>
<div class="upload-toggle-item">
<label class="switch">
<input type="checkbox" id="biounitCheckbox" checked>
<span class="slider-toggle"></span>
</label>
<label for="biounitCheckbox" class="upload-toggle-label">Load Biounit</label>
</div>
<div class="upload-toggle-item">
<label class="switch">
<input type="checkbox" id="loadLigandsCheckbox" checked>
<span class="slider-toggle"></span>
</label>
<label for="loadLigandsCheckbox" class="upload-toggle-label">Load Ligands</label>
</div>
<div class="upload-toggle-item">
<label class="switch">
<input type="checkbox" id="filterAdditivesCheckbox" checked>
<span class="slider-toggle"></span>
</label>
<label for="filterAdditivesCheckbox" class="upload-toggle-label"
title="Leave out buffers, cryoprotectants and counter-ions - what the crystal was grown in rather than what the molecule is">Filter Additives</label>
</div>
</div>
<!-- ...AND WHAT COMES WITH IT. Neither is the structure:
they are other files fetched alongside it, and they
open panels of their own. -->
<div class="fetch-option-group">
<span class="fetch-option-title">Alongside</span>
<div class="upload-toggle-item">
<label class="switch">
<input type="checkbox" id="loadPAECheckbox" checked>
<span class="slider-toggle"></span>
</label>
<label for="loadPAECheckbox" class="upload-toggle-label">Load PAE</label>
</div>
<div class="upload-toggle-item">
<label class="switch">
<input type="checkbox" id="loadMSACheckbox">
<span class="slider-toggle"></span>
</label>
<label for="loadMSACheckbox" class="upload-toggle-label">Load MSA</label>
</div>
</div>
</div>
</div>
<!-- Status message below the panel -->
<div id="status-message" style="margin-top: 8px;"></div>
</div>
<div id="viewer-container" class="py2dmol-viewer-instance" style="display: none;">
<div id="mainContainer">
<div id="viewerColumn">
<div id="canvasContainer" class="container-box">
<canvas id="canvas"></canvas>
<!-- Resize handle -->
<div class="resize-handle"></div>
</div>
<div id="controlsContainer" class="container-box">
<button id="playButton" class="btn btn-secondary btn-icon-only"><i
class="fa-solid fa-play"></i></button>
<input type="range" id="frameSlider" min="0" max="0" value="0">
<span id="frameCounter">0 / 0</span>
<button id="speedButton" class="btn btn-grey btn-icon-only"
title="Cycle playback speed">1x</button>
<button id="overlayButton" class="btn btn-secondary" title="Overlay all frames"
style="font-weight: 400;">⧉</button>
</div>
</div>
<!-- Right side panels container -->
<div id="rightPanelsContainer">
<div id="rightPanelContainer" class="container-box">
<!-- Toolbar (two rows) -->
<div id="navigationControls" class="toolbar" style="margin-bottom: 4px;">
<div class="toolbar-row">
<label class="btn-toggle btn-flex" id="orientToggle" title="Orient to best view">
<input type="checkbox" style="display: none;">
<span><i class="fa-solid fa-crosshairs"></i>Orient</span>
</label>
<!-- FOCUS IS A LATCH, so it says aria-pressed
rather than aria-expanded - the shared rule
in style.css lights either. Beside Orient
because the two are the camera moves a
CLICK makes: Orient frames everything,
Focus moves in on one thing.
NOT crosshairs: Orient wears those, right
beside it. A magnifier is what the mode
does - look closely at one thing - and it
is the only one on the page, so it cannot
be read as search. -->
<button id="focusButton" class="btn-toggle btn-flex"
aria-pressed="false"
title="Click a residue or ligand to draw its neighbours' side chains, move in on it and clip around it. The view does not turn. Click the background to come back out.">
<span><i class="fa-solid fa-magnifying-glass"></i>Focus</span>
</button>
<!-- Rotate: checkbox styled as a button -->
<label class="btn-toggle btn-flex" id="rotateToggle" title="Toggle auto-rotation">
<input type="checkbox" id="rotationCheckbox">
<span><i class="fa-solid fa-rotate"></i>Rotate</span>
</label>
</div>
<div class="toolbar-row">
<button id="styleToggle" class="btn-toggle btn-flex"
aria-expanded="false" aria-controls="stylePanel"
title="Render style and its settings">
<span><i class="fa-solid fa-palette"></i>Style</span>
</button>
<!-- Clip: PyMOL's near/far slab, cutting the drawing. -->
<label class="btn-toggle btn-flex" id="clipToggle"
title="Clip the view to a slab, as in PyMOL: near and far planes that cut the drawing">
<input type="checkbox" id="clipCheckbox">
<span><i class="fa-solid fa-cube"></i>Clip</span>
</label>
<button id="saveImageButton" class="btn-toggle btn-flex"
aria-expanded="false" aria-controls="savePanel"
title="Capture an image or a video">
<span><i class="fa-solid fa-camera"></i>Capture</span>
</button>
</div>
</div>
<!-- WHICH OBJECT, and how many. Directly under the
buttons because everything below it - Style, Clip,
Capture - acts on the object named here: pick an
object and the panels below describe THAT object.
Multi puts every object on screen at once; the
eyes decide what is drawn and the picker still
decides which one you are editing. -->
<!-- OBJECTS: ONE AT A TIME, OR SEVERAL.
The picker in the sequence header chooses the
object - one on screen, the one being edited,
which is how this viewer has always worked.
Multi is the other mode: the picker greys out,
every object gets a row with an eye, and the
eyes decide what is drawn. It opens on the
object the picker was showing, so pressing it
changes nothing until an eye is clicked. -->
<div id="objectRow" class="toggle-item object-row">
<!-- THE LIST, EXPANDED. Three stacked lines: the one
object on screen opening into all of them. It
read "Multi", which names a mode rather than
what the button does, and spent a fifth of a
narrow row saying it. The accessible name still
says it in words. -->
<button id="objectListButton" type="button"
class="object-list-btn" aria-pressed="false"
aria-controls="objectList" aria-label="Expand to several objects"
title="Expand: show several objects at once"><i
class="fa-solid fa-bars" aria-hidden="true"></i></button>
<!-- WHICH OBJECT: the picker, and the ordinary way to
choose one. It names the object on screen AND the one
being edited, which are the same object until Multi is
pressed - there it greys out, because with several on
screen the eyes decide what is drawn and clicking in
the strip decides what is edited (adoptObjectOfSelection).
Every other path drives the current object through this
select: the renderer's change listener, the prev/next
buttons, a restored session. -->
<select id="objectSelect" class="object-picker"
title="Which object to show and edit"
style="min-width: 110px; height: 28px; font-size: 12px; padding: 0 8px;">
<!-- Initially empty -->
</select>
</div>
<!-- Rows are built in src/app/objects.js from the objects that
exist: an eye and a name, and the whole row is
the switch. Only in Multi. -->
<div id="objectList" class="object-list" hidden></div>
<style>
/* Inline display:flex on the style rows outranks the UA [hidden]
rule, so force it. Scoped to the style panel + its trigger. */
#styleAppearanceContainer [hidden] { display: none !important; }
/* Open cue is the shared .btn-toggle[aria-expanded] rule
in style.css - the same green as a checked toggle. */
</style>
<!-- CLIP PANEL. PyMOL's clip: a slab in camera space, set as a
RANGE - two knobs on one track, far on the left and near on
the right, which cannot cross. No figures: where the knobs
are IS the answer, and the picture shows the rest.
Switching Clip off puts the panel away and LEAVES the cut;
AUTO is what re-cuts: it fits the slab to the selection,
and with nothing selected that is the uncut rest state -
which is what the button used to say Reset for. -->
<style>
/* [hidden] is a UA rule and an inline display:flex outranks
it - the same trap the style panel documents below. */
#clipPanel[hidden] { display: none !important; }
/* THE TWO HANDLES SHARE ONE TRACK. Both inputs sit on the
same line, transparent, so what you see is the track drawn
underneath and the two thumbs on it. pointer-events are
off for the input body and back on for the thumb, or the
upper input would swallow every click meant for the lower
one. */
/* TWO IDS, DELIBERATELY. src/app/style.css styles every range
input in the viewer with
`.py2dmol-viewer-instance input[type="range"]:not(#frameSlider)`,
and :not(#frameSlider) counts an ID - so that rule outranks
a single-id selector here and kept painting its own 6px grey
bar and its own blue thumb. That bar on top of this one is
the "two bars" this control was showing. */
#clipPanel #clipRange { position: relative; height: 24px; flex: 1; min-width: 0; }
#clipPanel #clipRange .track {
position: absolute; left: 0; right: 0; top: 10px; height: 4px;
background: #e5e7eb; border-radius: 2px;
}
#clipPanel #clipRange .span {
position: absolute; top: 10px; height: 4px;
background: #2563eb; border-radius: 2px;
}
#clipPanel #clipRange input[type=range] {
position: absolute; left: 0; top: 0; width: 100%; height: 24px;
margin: 0; padding: 0; background: transparent; pointer-events: none;
border-radius: 0; outline: none;
-webkit-appearance: none; appearance: none;
}
/* the track each input paints for itself, out of the way */
#clipPanel #clipRange input[type=range]::-webkit-slider-runnable-track {
background: transparent; border: none; height: 24px;
}
#clipPanel #clipRange input[type=range]::-moz-range-track {
background: transparent; border: none; height: 24px;
}
#clipPanel #clipRange input[type=range]::-moz-range-progress {
background: transparent;
}
#clipPanel #clipRange input[type=range]::-webkit-slider-thumb {
pointer-events: auto; -webkit-appearance: none; appearance: none;
width: 14px; height: 14px; margin-top: 5px; border-radius: 50%;
background: #fff; border: 2px solid #2563eb; box-shadow: none;
cursor: pointer;
}
#clipPanel #clipRange input[type=range]::-moz-range-thumb {
pointer-events: auto; width: 14px; height: 14px; border-radius: 50%;
background: #fff; border: 2px solid #2563eb; box-shadow: none;
cursor: pointer;
}
</style>
<!-- TWO LINES: the planes, then how soft they are. Each row
spells out flex-direction ROW - the panel is a COLUMN and
.control-group is one too, and inheriting that once stacked
the readouts above a track with no width at all: the handles
were there and unusable. -->
<div id="clipPanel" hidden class="control-group"
style="display: flex; flex-direction: column; align-items: stretch; gap: 6px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; padding: 8px; margin-bottom: 4px;">
<div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
<label for="clipNear" class="font-medium text-gray-700"
style="width: 34px; font-size: 11px; flex-shrink: 0;"
title="Drag the two knobs to clip the view between them">Clip:</label>
<div id="clipRange" title="Drag the two knobs to clip the view between them">
<div class="track"></div>
<div class="span" id="clipSpan"></div>
<input type="range" id="clipFar" aria-label="Far clipping plane">
<input type="range" id="clipNear" aria-label="Near clipping plane">
</div>
<button id="clipAutoButton" class="btn btn-grey btn-small"
title="Fit the slab to the selection - or uncut, with nothing selected"
style="font-size: 11px;">Auto</button>
</div>
<div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
<label for="clipFadeSlider" class="font-medium text-gray-700"
style="width: 34px; font-size: 11px; flex-shrink: 0;"
title="Soft edge: how far outside each plane the drawing fades out instead of stopping. 0 is a hard cut.">Fade:</label>
<input type="range" id="clipFadeSlider" min="0" max="100" value="10" step="5"
style="flex: 1; min-width: 0; outline: none;"
title="Soft edge: how far outside each plane the drawing fades out instead of stopping. 0 is a hard cut.">
<!-- Auto stands over this space on the row above; without
it the two tracks would end in different places, which
is the whole reason the rows were split. -->
<span aria-hidden="true" id="clipResetSpacer"
style="width: 51px; flex-shrink: 0;"></span>
</div>
</div>
<div id="styleAppearanceContainer" class="control-group">
<!-- The Style panel is BUILT by parts/panel.js and inserted here by
ui.js. It used to be written out in full in this file AND again
in the other page, under a note saying to edit both - which had
already been missed in both directions. -->
<div id="stylePanelMount"></div>
</div>
</div>
<!-- SELECTION PANEL. These tools act on the 3D structure,
not on the sequence, and a selection can just as well be
made by clicking the canvas or the PAE map - so their
home in the sequence header was never quite right.
Here they sit beside the thing they change.
Hidden until something is selected: with nothing picked
every one of them is a no-op. -->
<div id="selectionPanel" class="container-box selection-panel" hidden>
<!-- THE HEAD CARRIES THE ACTIONS, as icons. Copy used to be a
full-width button at the bottom of the panel, where it sat
under the pointer after every other control and was pressed
by accident. Small, cornered and iconic, it takes a
deliberate move to reach - which is what an action that
makes a new object should take. -->
<div class="selection-panel-head">
<span class="selection-panel-title">Selection</span>
<span id="selectionPanelCount" class="selection-panel-count"></span>
<span class="selection-panel-actions">
<button id="copySelectionButton" class="selection-icon-btn"
title="Copy the selected residues into a new object"
aria-label="Copy the selection into a new object">
<i class="fa-regular fa-copy"></i>
</button>
<!-- CUT is Copy and Delete in one press, and it
has to be one: Copy switches to the object it
makes and Delete works on whatever is current,
so pressing them in turn deletes the copy out
of itself and leaves the original whole. -->
<button id="cutSelectionButton" class="selection-icon-btn"
title="Cut the selected residues into a new object, removing them from this one"
aria-label="Cut the selection into a new object">
<i class="fa-solid fa-scissors"></i>
</button>
<button id="deleteSelectionButton" class="selection-icon-btn selection-icon-danger"
title="Delete the selected residues from this object"
aria-label="Delete the selected residues">
<i class="fa-solid fa-trash-can"></i>
</button>
</span>
</div>
<!-- WHAT IS DRAWN, one labelled row per part, because
the two are independent: a residue can show its main
chain, its side chain, both or neither.
Show and Hide are two buttons rather than one
switch. A switch says what it will do and leaves
you to work out what it has done; a pair says both,
because the one matching what is drawn is filled -
and a selection that disagrees with itself fills
neither, which is a state a switch can only show as
a grey smear. Plate and Elements stay switches: they
are not "is this drawn" but "which way", and a
second pair on the same row would read as a second
copy of the same question. -->
<div id="selectionTools" class="selection-tools" title="Applies to the selected residues">
<!-- Hidden when the selection has no side chains to
show - all glycine, nucleic acid, or a
backbone-only model. See hasSidechainsFor. -->
<div class="selection-panel-row" id="sidechainRow">
<span class="selection-panel-label">Side chains</span>
<span class="selection-color-wrap">
<!-- The swatch IS the label: a coloured square
next to Show/Hide reads as "colour"
without saying so, and the word cost a
third of the row in a 340px panel. -->
<button type="button" id="scColorButton" class="btn btn-grey btn-small selection-color"
aria-label="Colour the selected side chains"
title="Colour the selected side chains">
<span id="scColorSwatch" class="selection-swatch"></span>
</button>
<div id="scColorMenu" class="selection-color-menu" hidden></div>
</span>
<!-- TWO BUTTONS, ONE QUESTION. A single switch
said what it would do and left you to work
out what it had done; a pair says both -
the one that matches what is drawn is
filled, and neither is when the selection
disagrees with itself. Which is the third
state a switch could only show as a grey
smear. -->
<span class="selection-switch" id="sidechainPair"
role="group" aria-label="Draw side chains for the selected residues">
<button type="button" id="sidechainShowButton"
class="selection-switch-btn"
title="Draw side chains for the selected residues">Show</button>
<button type="button" id="sidechainHideButton"
class="selection-switch-btn"
title="Hide the side chains of the selected residues">Hide</button>
</span>
<!-- ...AND HOW, FOR A NUCLEOTIDE, which is the only
thing with two ways of being drawn: the plate or
its real atoms. A switch rather than a menu,
because Plate is on or it is not and a two-item
menu is a switch that takes a click to read -
and because the menu was wide enough to push
Elements onto a second line. It appears only
where the selection has nucleotides, and only
while they are being drawn. -->
<label class="btn-toggle btn-small selection-toggle" title="Draw the bases as plates rather than atoms">
<input type="checkbox" id="plateShowToggle"
aria-label="Draw the selected bases as plates">
<span>Plate</span>
</label>
<!-- Element colours ride WITH the side chains rather than in a
row of their own: they are a property of the atoms this row
draws, and a row that only ever applies to whatever this one
turned on reads as a separate thing to manage. -->
<label class="btn-toggle btn-small selection-toggle" title="Colour the selected atoms by element - nitrogen blue, oxygen red, sulfur gold">
<input type="checkbox" id="elementsShowToggle"
aria-label="Colour the selected atoms by element">
<!-- "Elem", not "Elements": the row holds a
caption, Show, Hide and this, and the
full word pushed it onto a second line
in a 340px column. The title says what
it is in full, and the aria-label with
it, so nothing that reads the page is
left with the abbreviation. -->
<span>Elem</span>
</label>
</div>
<div class="selection-panel-row" id="mainchainRow">
<span class="selection-panel-label">Main chain</span>
<span class="selection-color-wrap">
<!-- aria-label because the button has no text
left: title is only a fallback for an
accessible name, so dropping the word
would otherwise have left these two
unnamed to a screen reader. -->
<button type="button" id="selColorButton" class="btn btn-grey btn-small selection-color"
aria-label="Colour the selected residues"
title="Colour the selected residues (PyMOL palette)">
<span id="selColorSwatch" class="selection-swatch"></span>
</button>
<div id="selColorMenu" class="selection-color-menu" hidden></div>
</span>
<!-- SHOW IS THE CHAIN, and nothing else. It used
to hide the whole residue, side chain and
all, with a second toggle beside it for the
backbone alone - two controls where one
said "draw this part", which is what every
other row on this panel means by Show. A
residue with no chain and no side chain
draws nothing, and drops out of the
visibility mask on its own. -->
<span class="selection-switch" id="mainchainPair"
role="group" aria-label="Draw the backbone of the selected residues">
<button type="button" id="mainchainShowButton"
class="selection-switch-btn"
title="Draw the backbone of the selected residues">Show</button>
<button type="button" id="mainchainHideButton"
class="selection-switch-btn"
title="Hide the backbone. Their side chains, bases and contacts stay.">Hide</button>
</span>
<!-- SSE lives HERE because it is a BACKBONE
property: a side chain has no secondary
structure. It sits where Elements sits on
the side-chain row - the second control on
the row, styling what that row draws. -->
<!-- A STATE, NOT AN ACTION. It read "SSE" whatever
the selection was, because it reset itself
after every pick - so the one thing a menu
on a selection panel is for, saying what the
selection currently is, was the one thing it
did not do. It now shows which of the four
states the selection is in, and Mixed where
they disagree.
DSSP rather than Auto: the automatic answer
is not a preference, it is what the
assignment says, and naming it says which
assignment. Its label carries that answer
where the drawing already knows it. -->
<select id="selSsSelect" class="btn btn-grey btn-small"
title="The secondary structure of the selected residues">
<option value="" disabled hidden>Mixed</option>
<option value="dssp">DSSP</option>
<option value="H">Helix</option>
<option value="E">Sheet</option>
<option value="C">Loop</option>
</select>
</div>
<!-- WHAT THE SELECTION IS, above; WHAT TO DO WITH IT,
below. The rows above set properties of the residues
you have picked - how they are drawn, what colour,
which structure - and read as a group because they
all answer "what does this look like". Find and
Contact do something instead: one changes the
selection, the other adds an annotation. Running
them into the same stack of labelled rows made
Find look like another property with a button
next to it. -->
<div class="selection-panel-divider" id="selActionDivider"></div>
<!-- FIND INTERACTIONS: the residues whose SIDE CHAINS
come within 5 A of the selection, atom to atom, side
chains rebuilt whether or not they are drawn.
One button and no settings, because the settings were
a distance nobody wanted to change and a choice
between "any atom" and "side chain" whose any-atom
half is mostly backbone running past. It belongs on
this panel rather than beside Select all: those make
or clear a selection, this acts on one - and what you
do with the answer is turn on the side chains two rows
up. The seed stays selected, as PyMOL's byres does. -->
<div class="selection-panel-row" id="nearbyRow">
<span class="selection-panel-label">Find</span>
<button id="selectNearby" class="btn btn-grey btn-small"
title="Select every residue whose side chain comes within 5 Å of the selection">interactions</button>
</div>
<!-- ALIGN: superpose other objects onto the SELECTED
RESIDUES by TM-align. It sits on this panel and
next to Find because it is the same kind of thing
- an action on the selection rather than a
property of it - and because what you align to is
the pick: one chain out of a complex, or one
domain out of a chain, is a selection and nothing
else in the app can say it.
A DROPDOWN AND NOT TWO BUTTONS: "all" and
"visible" are the same action over different
sets, so they read as one control with a choice,
and Undo belongs in the same list because it is
the third thing you can do to an alignment. The
row is hidden until a second object is loaded -
there is nothing to align one structure to. -->
<div class="selection-panel-row" id="alignRow" hidden>
<span class="selection-panel-label">Align</span>
<select id="alignSelect" class="btn btn-grey btn-small"
title="Superpose other objects onto the selected residues (TM-align)">
<option value="" selected hidden>to selection</option>
<option value="all">all to this</option>
<option value="visible">visible to this</option>
<option value="none">undo</option>
</select>
</div>
<!-- CONTACTS need exactly two residues, so this row
appears only then - a contact is a line between
a pair and there is nothing to draw for one
residue or for five. -->
<div class="selection-panel-row" id="contactRow" hidden>
<span class="selection-panel-label">Contact</span>
<span class="selection-color-wrap">
<button type="button" id="contactColorButton"
class="btn btn-grey btn-small selection-color"
aria-label="Colour the contact between the selected residues"
title="Colour the contact between the selected residues">
<span id="contactColorSwatch" class="selection-swatch"></span>
</button>
<div id="contactColorMenu" class="selection-color-menu" hidden></div>
</span>
<!-- ADD, not Show: a contact does not exist
until you make one, and once it does the
button has nothing left to offer - so it
goes, and the row becomes the contact's own
controls: its colour, its width, and a bin
to be rid of it. One control per state
rather than a pair with a dead half. -->
<button type="button" id="contactAddButton"
class="btn btn-grey btn-small selection-add"
title="Draw a contact between the two selected residues">Add</button>
<!-- Per contact, not global: the stored entry
carries its own weight, so two contacts in
one picture can differ. Narrow, to keep the
row to one line.
FULL WIDTH IS THE MAXIMUM. The slider only
takes a contact down from the width it is
drawn at, rather than letting it grow past
it - an annotation that can outweigh the
structure it annotates is not useful. -->
<input type="range" id="contactWidthSlider" class="selection-mini-slider"
min="0.15" max="1" step="0.05" value="1" hidden
aria-label="Width of this contact"
title="Width of this contact (full width is the maximum)">
<!-- ...AND THE BIN, at the end of the row: the
last thing on a row of that contact's own
controls, where a destructive action is a
deliberate reach rather than the button
next to the one you were just pressing. -->
<button type="button" id="contactDeleteButton"
class="btn btn-grey btn-small selection-icon-btn selection-icon-danger"
hidden
aria-label="Remove the contact between the two selected residues"
title="Remove this contact">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</div>
</div>
<!-- PAE Canvas Container -->
<div id="paeContainer" class="container-box" style="display: none;">
<canvas id="paeCanvas" style="cursor: crosshair;"></canvas>
</div>
<!-- Scatter Plot Container (mirrors main viewer sizing; JS sets dimensions) -->
<div id="scatterContainer" class="container-box" style="display: none; position: relative;">
<canvas id="scatterCanvas" style="cursor: pointer;"></canvas>
<div class="resize-handle" aria-label="Resize scatter plot"></div>
</div>
</div>
</div>
</div>
<!-- Sequence container with box and toggle -->
<div id="sequence-viewer-container" class="container-box sequence-section-container"
style="width: 948px; display: none;">
<div id="sequenceHeader" class="sequence-header">
<!-- Left side: Mode dropdown -->
<div style="display: flex; align-items: center; gap: 8px;">
<select id="sequenceModeSelect" class="btn btn-grey btn-small"
style="min-width: 100px; height: 28px; font-size: 12px; padding: 0 8px;">
<option value="sequence" selected>Sequence</option>
<option value="chain">Chain</option>
</select>
<!-- The object picker lives beside the Multi button, in the
Objects row - see #objectRow. -->
</div>
<!-- WHICH FILE THIS FRAME CAME FROM, in the gap between the mode
dropdown and the buttons. Loading a folder of predictions as
frames gives one object and a frame number; this says which
file you are looking at. Empty when the frames have no names
of their own (a single structure, or a session from before
they were kept). -->
<div id="frameNameLabel"
style="flex: 1; min-width: 0; text-align: center; font-size: 12px; color: #6b7280; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
<!-- Action buttons aligned to the right -->
<div class="sequence-actions" style="margin-left: auto;">
<!-- These CREATE a selection, so they cannot live in the
selection panel - that panel only exists once there is
one. They stay next to the strip you select in. -->
<span id="selectionGlobalTools" class="selection-global-tools">
<button id="selectAllResidues" class="btn btn-grey btn-small"
title="Select every residue">Select all</button>
<button id="clearAllResidues" class="btn btn-grey btn-small"
title="Clear the selection">Unselect</button>
<button id="invertSelection" class="btn btn-grey btn-small"
title="Select everything that is not selected now">Invert</button>
</span>
</div>
</div>
<div id="sequenceView" class="sequence-content hidden"></div>
</div>
<!-- MSA container with box and toggle -->
<div id="msa-buttons" class="sequence-section-container" style="display: none;">
<div id="msaHeader" class="sequence-header">
<!-- Left side: Mode dropdown, Save buttons, Sort checkbox, Bit-score checkbox -->
<div style="display: flex; align-items: center; gap: 8px;">
<select id="msaModeSelect" class="btn btn-grey btn-small"
style="min-width: 100px; height: 28px; font-size: 12px; padding: 0 8px;">
<option value="msa" selected>MSA</option>
<option value="pssm">PSSM</option>
<option value="logo">Logo</option>
<option value="coverage">Coverage</option>
</select>
<div id="msaSaveContainer" style="display: none; align-items: center; gap: 4px;">
<button id="msaSaveFastaButton" class="btn btn-grey btn-small" style="font-size: 11px;">Save
Fasta</button>
</div>
<div id="logoSaveContainer" style="display: none; align-items: center; gap: 4px;">
<button id="logoSaveSvgButton" class="btn btn-grey btn-small" style="font-size: 11px;">Save
SVG</button>
</div>
<div id="pssmSaveContainer" style="display: none; align-items: center; gap: 4px;">
<button id="pssmSaveSvgButton" class="btn btn-grey btn-small" style="font-size: 11px;">Save
SVG</button>
<button id="pssmSaveCsvButton" class="btn btn-grey btn-small" style="font-size: 11px;">Save
CSV</button>
</div>
<div id="msaSortContainer" style="display: flex; align-items: center; gap: 4px;">
<label for="msaSortCheckbox"
style="font-size: 12px; color: #666; white-space: nowrap; display: flex; align-items: center; gap: 4px; cursor: pointer;">
<input type="checkbox" id="msaSortCheckbox" checked style="cursor: pointer;">
<span>Sort</span>
</label>
</div>
<div id="logoBitScoreContainer" style="display: none; align-items: center; gap: 4px;">
<label for="logoBitScoreCheckbox"
style="font-size: 12px; color: #666; white-space: nowrap; display: flex; align-items: center; gap: 4px; cursor: pointer;">
<input type="checkbox" id="logoBitScoreCheckbox" checked style="cursor: pointer;">
<span>Bit-score</span>
</label>
</div>
</div>
<!-- Right: Filters box and Chain selector -->
<div style="display: flex; align-items: center; gap: 8px; margin-left: auto;">
<div
style="display: flex; align-items: center; gap: 10px; padding: 4px 10px; border: 1px solid #ccc; border-radius: 4px; background: #f9f9f9;">
<span style="font-size: 12px; color: #666; font-weight: 500;">Filters:</span>
<div style="display: flex; align-items: center; gap: 4px;">
<label for="coverageSlider"
style="font-size: 11px; color: #666; white-space: nowrap;">cov:</label>
<input type="range" id="coverageSlider" min="0" max="100" value="75" step="5"
style="width: 100px;">
<span id="coverageValue"
style="font-size: 11px; color: #666; width: 40px; text-align: right; display: inline-block;">75%</span>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<label for="identitySlider"
style="font-size: 11px; color: #666; white-space: nowrap;">qid:</label>
<input type="range" id="identitySlider" min="0" max="100" value="15" step="5"
style="width: 100px;">
<span id="identityValue"
style="font-size: 11px; color: #666; width: 40px; text-align: right; display: inline-block;">15%</span>
</div>
<span id="msaSequenceCount"
style="font-size: 11px; color: #666; white-space: nowrap; min-width: 80px; text-align: center; margin-left: 4px; display: inline-block;">-</span>
</div>
<!-- Chain selector (hidden by default, shown when multiple chains available) -->
<select id="msaChainSelect" class="btn btn-grey btn-small"
style="display: none; min-width: 50px; height: 28px; font-size: 12px; padding: 0 8px;"></select>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer style="margin-top: var(--container-gap); padding: 0; color: #6b7280; font-size: 13px;">
<div
style="width: 948px; margin: 0 auto; padding: 10px 0 0 0; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; gap: 20px; flex-wrap: wrap;">
<div style="flex: 0 0 auto; display: flex; align-items: center; gap: 12px;">
<a href="https://github.com/sokrypton/py2Dmol" target="_blank" rel="noopener noreferrer"
style="color: #3b82f6; text-decoration: none;">
<i class="fab fa-github" style="margin-right: 6px;"></i>View on GitHub
</a>
</div>
<div style="flex: 0 0 auto; font-size: 12px;">
<i class="fa-solid fa-shield-halved" style="margin-right: 6px; color: #10b981;"></i>
<strong>Privacy:</strong> All processing is performed locally in your browser. No data is uploaded to a
server.
</div>
</div>
</footer>
<!-- THE DEPLOYED PAGE LOADS THE BUNDLE. py2dmol.solab.org serves this
file from the repo root, so twenty-seven tags meant twenty-seven
requests and 2.9 MB; bundles/py2Dmol.web.min.js is 729 KB in one. To develop
against the loose sources - real line numbers, no rebuild between an
edit and a reload - open dev.html, which tools/bundle.py generates
from this file. -->
<!-- TM-align and the viewer's side of it, in one file, and the one thing
that can never be concatenated: it starts its Worker by having the
worker importScripts *itself*, found through document.currentScript.src,
which inlining leaves empty. Its TM-align half is generated from
foldjs/lib/tmalign.js - see tests/vendor_tmalign.mjs. -->
<script src="src/align/align.js"></script>
<!-- GENERATED by tools/bundle.py from index.html. Do not edit:
change index.html and run `python3 tools/bundle.py build`.
This is index.html with the bundle expanded into the loose
sources it was built from - the page to develop against. -->
<script src="src/io/math.js"></script>
<script src="src/io/sidechains.js"></script>
<script src="src/io/bonds.js"></script>
<script src="src/io/parse.js"></script>
<script src="src/io/gif.js"></script>
<script src="src/core/svg.js"></script>
<script src="src/core/objstate.js"></script>
<script src="src/parts/viewport.js"></script>
<script src="src/parts/shadow.js"></script>
<script src="src/parts/clip.js"></script>
<script src="src/parts/focus.js"></script>
<script src="src/parts/sidechains.js"></script>
<script src="src/parts/capture.js"></script>
<script src="src/parts/savepanel.js"></script>
<script src="src/parts/align.js"></script>
<script src="src/parts/multi.js"></script>
<script src="src/parts/panel.js"></script>
<script src="src/parts/orient.js"></script>
<script src="src/parts/ui.js"></script>
<script src="src/core/mol.js"></script>
<script src="src/cartoon/geom.js"></script>
<script src="src/cartoon/paint2d.js"></script>
<script src="src/cartoon/paintgl.js"></script>
<script src="src/panels/pae.js"></script>
<script src="src/panels/scatter.js"></script>
<script src="src/panels/seq.js"></script>
<script src="src/panels/msa.js"></script>
<script src="src/app/main.js"></script>
<script src="src/app/selection.js"></script>
<script src="src/app/objects.js"></script>
<script src="src/app/fetch.js"></script>
<script src="src/app/scatter.js"></script>
<script src="src/app/session.js"></script>
</body>
</html>