Repository navigation
Expand file tree
/
Copy pathdev.html
More file actions
788 lines (733 loc) · 47.8 KB
/
Copy pathdev.html
File metadata and controls
788 lines (733 loc) · 47.8 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
<!--
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 -->
<!-- .page-width is the shell: 600 (canvas) + 8 (gap) + 340 (panel). It was
written inline on four elements, which is a width no media query can
override - see the banner in src/parts/viewport.js. It is a class now,
in src/app/style.css, and the narrow layout overrides that one rule. -->
<div class="mx-auto page-width" style="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. -->
<!-- ONE BAR: the title and the three page-wide actions. The actions are
absolutely positioned inside it on the desktop, exactly as they were
when they were positioned against the whole page - out of flow, so
the title's own line is unchanged. At a narrow width the bar becomes
an ordinary wrapping flex row and they stop overlapping the title,
which is what they did: `position: absolute; right: 0` slides them
left over "2Dmol" as soon as there is not room for both. -->
<div class="page-topbar">
<div class="page-head">
<h1>2Dmol</h1>
</div>
<div class="page-actions">
<!-- 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>
</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 id="upload-options-container" class="container-box page-width" style="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">
<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">
<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">
<!-- data-autosize="css": THE STYLESHEET OWNS THIS BOX, so setupViewport
writes no inline width and a media query can
reach it. The notebook deliberately does NOT set
this - see the banner in src/parts/viewport.js. -->
<div id="canvasContainer" class="container-box" data-autosize="css">
<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.
IT IS BUILT AT RUNTIME, from parts/panel.js, the way
the Style panel above it is. It was two hundred lines of
markup here and nowhere else, which left the verbs in
parts/selectpanel.js in every download with one page able
to reach them. This element says WHERE it goes; the
panel decides what is on it, and a selection is what
opens it. -->
<div id="selectionPanelMount"></div>
<!-- PAE Canvas Container -->
<div id="heatmapContainer" class="container-box" style="display: none;">
<canvas id="heatmapCanvas" style="cursor: crosshair;"></canvas>
</div>
<!-- The confidence trace, one column per residue of the drawn
frame. Hidden until parts/slots.js parks it: the panel
mounts only when the data is not a flat pad, so a
crystal structure never grows the tab. -->
<div id="plddtContainer" class="container-box" style="display: none;"></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 page-width"
style="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 class="page-width"
style="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 1 auto; min-width: 0; 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 1 auto; min-width: 0; 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/selectpanel.js"></script>
<script src="src/parts/orient.js"></script>
<script src="src/parts/slots.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/heatmap.js"></script>
<script src="src/panels/plddt.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/objects.js"></script>
<script src="src/app/fetch.js"></script>
<script src="src/app/scatter.js"></script>
<script src="src/app/session.js"></script>
<div id="devRebuildLight" aria-hidden="true"></div>
<style>
#devRebuildLight {
position: fixed; top: 8px; right: 10px; z-index: 99999;
pointer-events: none; /* never in the way of a click */
font: 600 13px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
color: #c0392b; background: rgba(255,255,255,0.82);
border: 1px solid rgba(192,57,43,0.45); border-radius: 3px;
padding: 2px 5px; opacity: 0; transition: opacity 140ms ease-out;
}
#devRebuildLight.on { opacity: 1; transition: none; }
#devRebuildLight.sc { color: #d35400; border-color: rgba(211,84,0,0.45); }
</style>
<script>
(function () {
var el = document.getElementById('devRebuildLight');
if (!el) return;
var seen = 0, off = 0;
// A COUNT AS WELL AS A FLASH. At playback speed a flash you miss is a flash
// that told you nothing, and "R x18" over a run is the number worth reading.
function tick() {
var n = window.__faceBuilds || 0;
if (n !== seen) {
if (n > seen) {
var rb = window.__rebuild || {};
var ribReused = !!rb.ribbonReused;
if (ribReused) {
var sc = window.__sidechainBuilds || 0;
el.textContent = 'SC' + (sc > 1 ? ' ×' + sc : '');
el.title = 'Sidechains rebuilt (' + sc + '), ribbon 100% reused (0 ms)';
el.classList.add('sc');
} else {
var rib = window.__ribbonBuilds || n;
el.textContent = 'R' + (rib > 1 ? ' ×' + rib : '');
el.title = rib + ' cartoon ribbon rebuild' + (rib === 1 ? '' : 's');
el.classList.remove('sc');
}
el.classList.add('on');
clearTimeout(off);
off = setTimeout(function () { el.classList.remove('on'); }, 420);
}
seen = n;
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
})();
</script>
</body>
</html>