|
75 | 75 | flex-shrink: 0; |
76 | 76 | font-size: 12px; /* Ensure label font size matches */ |
77 | 77 | } |
78 | | - /* Style for the shadow/outline checkbox labels */ |
| 78 | + /* Style for the shadow/outline/pastel checkbox labels */ |
79 | 79 | .toggle-item label[for="shadowEnabledCheckbox"], |
80 | | - .toggle-item label[for="outlineEnabledCheckbox"] { |
| 80 | + .toggle-item label[for="outlineEnabledCheckbox"], |
| 81 | + .toggle-item label[for="rotationCheckbox"] { |
81 | 82 | width: auto; /* Let it be auto-sized */ |
82 | 83 | margin-left: 5px; /* Add space next to the slider */ |
83 | 84 | } |
|
216 | 217 | <!-- Right Column --> |
217 | 218 | <div id="rightPanelContainer"> |
218 | 219 |
|
219 | | - <!-- Color Select --> |
220 | | - <div class="control-group"> |
221 | | - <label for="colorSelect">Color</label> |
222 | | - <select id="colorSelect"> |
223 | | - <option value="auto">Auto</option> |
224 | | - <option value="plddt">pLDDT</option> |
225 | | - <option value="rainbow">Rainbow</option> |
226 | | - <option value="chain">Chain</option> |
227 | | - </select> |
228 | | - </div> |
229 | | - |
230 | | - <!-- Options --> |
231 | | - <div id="optionsContainer" class="control-group"> |
232 | | - <div class="toggle-item"> |
233 | | - <input type="checkbox" id="rotationCheckbox"> |
234 | | - <label for="rotationCheckbox">Rotate</label> |
| 220 | + <!-- Style Group --> |
| 221 | + <div id="styleAppearanceContainer" class="control-group"> |
| 222 | + <label>Style</label> |
| 223 | + <!-- Color Select --> |
| 224 | + <div class="toggle-item"> |
| 225 | + <label for="colorSelect" style="width: 50px;">Color:</label> |
| 226 | + <select id="colorSelect" style="flex-grow: 1;"> |
| 227 | + <option value="auto">Auto</option> |
| 228 | + <option value="plddt">pLDDT</option> |
| 229 | + <option value="rainbow">Rainbow</option> |
| 230 | + <option value="chain">Chain</option> |
| 231 | + </select> |
| 232 | + </div> |
| 233 | + <!-- Pastel --> |
| 234 | + <div class="toggle-item"> |
| 235 | + <label for="pastelSlider" style="width: 50px;">Pastel:</label> |
| 236 | + <input type="range" id="pastelSlider" min="0" max="1" value="0.25" step="0.05"> |
235 | 237 | </div> |
| 238 | + <!-- Width --> |
236 | 239 | <div class="toggle-item"> |
237 | | - <label for="lineWidthSlider">Width:</label> |
| 240 | + <label for="lineWidthSlider" style="width: 50px;">Width:</label> |
238 | 241 | <input type="range" id="lineWidthSlider" min="1" max="5" value="3" step="0.5"> |
239 | 242 | </div> |
| 243 | + <!-- Shadow --> |
240 | 244 | <div class="toggle-item"> |
241 | 245 | <input type="checkbox" id="shadowEnabledCheckbox"> |
242 | 246 | <label for="shadowEnabledCheckbox">Shadow</label> |
|
248 | 252 | </div> |
249 | 253 | </div> |
250 | 254 |
|
251 | | - <!-- Trajectory --> |
| 255 | + <!-- View Group --> |
| 256 | + <div id="viewContainer" class="control-group"> |
| 257 | + <label>View</label> |
| 258 | + <div class="toggle-item"> |
| 259 | + <input type="checkbox" id="rotationCheckbox"> |
| 260 | + <label for="rotationCheckbox">Rotate</label> |
| 261 | + </div> |
| 262 | + </div> |
| 263 | + |
| 264 | + <!-- Trajectory Group --> |
252 | 265 | <div id="trajectoryContainer" class="control-group"> |
253 | 266 | <label for="trajectorySelect">Trajectory</label> |
254 | 267 | <select id="trajectorySelect"> |
|
289 | 302 | const pymolColors = ["#33ff33","#00ffff","#ff33cc","#ffff00","#ff9999","#e5e5e5","#7f7fff","#ff7f00","#7fff7f","#199999","#ff007f","#ffdd5e","#8c3f99","#b2b2b2","#007fff","#c4b200","#8cb266","#00bfbf","#b27f7f","#fcd1a5","#ff7f7f","#ffbfdd","#7fffff","#ffff7f","#00ff7f","#337fcc","#d8337f","#bfff3f","#ff7fff","#d8d8ff","#3fffbf","#b78c4c","#339933","#66b2b2","#ba8c84","#84bf00","#b24c66","#7f7f7f","#3f3fa5","#a5512b"]; |
290 | 303 | function hexToRgb(hex) { if (!hex || typeof hex !== 'string') { return {r: 128, g: 128, b: 128}; } const r = parseInt(hex.slice(1,3), 16); const g = parseInt(hex.slice(3,5), 16); const b = parseInt(hex.slice(5,7), 16); return {r, g, b}; } |
291 | 304 | function hsvToRgb(h, s, v) { const c = v * s; const x = c * (1 - Math.abs((h / 60) % 2 - 1)); const m = v - c; let r, g, b; if (h < 60) { r = c; g = x; b = 0; } else if (h < 120) { r = x; g = c; b = 0; } else if (h < 180) { r = 0; g = c; b = x; } else if (h < 240) { r = 0; g = x; b = c; } else if (h < 300) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return { r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255) }; } |
| 305 | + |
| 306 | + // N-term (blue) to C-term (red) |
292 | 307 | function getRainbowColor(value, min, max) { |
293 | 308 | if (max - min < 1e-6) return hsvToRgb(240, 1.0, 1.0); // Default to blue |
294 | 309 | let normalized = (value - min) / (max - min); |
295 | 310 | normalized = Math.max(0, Math.min(1, normalized)); |
296 | | - const hue = 240 * (1 - normalized); // Reversed: 0 -> 240 (blue), 1 -> 0 (red) |
| 311 | + const hue = 240 * (1 - normalized); // 0 -> 240 (blue), 1 -> 0 (red) |
297 | 312 | return hsvToRgb(hue, 1.0, 1.0); |
298 | 313 | } |
299 | | - function getPlddtColor(plddt) { return getRainbowColor(plddt, 50, 90); } |
| 314 | + |
| 315 | + // 50 (red) to 90 (blue) |
| 316 | + function getPlddtRainbowColor(value, min, max) { |
| 317 | + if (max - min < 1e-6) return hsvToRgb(0, 1.0, 1.0); // Default to red |
| 318 | + let normalized = (value - min) / (max - min); |
| 319 | + normalized = Math.max(0, Math.min(1, normalized)); |
| 320 | + const hue = 240 * normalized; // 0 -> 0 (red), 1 -> 240 (blue) |
| 321 | + return hsvToRgb(hue, 1.0, 1.0); |
| 322 | + } |
| 323 | + |
| 324 | + function getPlddtColor(plddt) { return getPlddtRainbowColor(plddt, 50, 90); } |
300 | 325 | function getChainColor(chainIndex) { if (chainIndex < 0) chainIndex = 0; return hexToRgb(pymolColors[chainIndex % pymolColors.length]); } |
301 | 326 |
|
302 | 327 | // ============================================================================ |
303 | 328 | // PSEUDO-3D RENDERER |
304 | | - // ============================================================================ |
305 | 329 | class Pseudo3DRenderer { |
306 | 330 | constructor(canvas) { |
307 | 331 | this.canvas = canvas; |
|
317 | 341 | default_rotate: false, |
318 | 342 | hide_controls: false, |
319 | 343 | autoplay: false, |
320 | | - hide_box: false |
| 344 | + hide_box: false, |
| 345 | + default_pastel: 0.25 |
321 | 346 | }; |
322 | 347 |
|
323 | 348 | // Current render state |
|
336 | 361 | // Set defaults from config, with fallback |
337 | 362 | this.shadowEnabled = (typeof config.default_shadow === 'boolean') ? config.default_shadow : true; |
338 | 363 | this.outlineEnabled = (typeof config.default_outline === 'boolean') ? config.default_outline : true; |
| 364 | + this.pastelLevel = (typeof config.default_pastel === 'number') ? config.default_pastel : 0.25; |
339 | 365 |
|
340 | 366 | this.isTransparent = false; // Default to white background |
341 | 367 | this.resolvedAutoColor = 'rainbow'; // Default 'auto' to rainbow |
|
389 | 415 | this.lineWidthSlider = null; |
390 | 416 | this.shadowEnabledCheckbox = null; |
391 | 417 | this.outlineEnabledCheckbox = null; |
| 418 | + this.pastelSlider = null; |
392 | 419 |
|
393 | 420 | this.setupInteraction(); |
394 | 421 | } |
|
403 | 430 | const dy = touch1.clientY - touch2.clientY; |
404 | 431 | return Math.sqrt(dx * dx + dy * dy); |
405 | 432 | } |
| 433 | + |
| 434 | + _applyPastel(rgb) { |
| 435 | + if (this.pastelLevel <= 0) { |
| 436 | + return rgb; |
| 437 | + } |
| 438 | + // Apply pastel transformation (mix with white) |
| 439 | + const mix = this.pastelLevel; |
| 440 | + return { |
| 441 | + r: Math.round(rgb.r * (1 - mix) + 255 * mix), |
| 442 | + g: Math.round(rgb.g * (1 - mix) + 255 * mix), |
| 443 | + b: Math.round(rgb.b * (1 - mix) + 255 * mix) |
| 444 | + }; |
| 445 | + } |
406 | 446 |
|
407 | 447 | setupInteraction() { |
408 | 448 | // Add inertia logic |
|
568 | 608 | } |
569 | 609 |
|
570 | 610 | // Set UI controls from main script |
571 | | - setUIControls(controlsContainer, playButton, frameSlider, frameCounter, trajectorySelect, speedSelect, rotationCheckbox, lineWidthSlider, shadowEnabledCheckbox, outlineEnabledCheckbox) { |
| 611 | + setUIControls(controlsContainer, playButton, frameSlider, frameCounter, trajectorySelect, speedSelect, rotationCheckbox, lineWidthSlider, shadowEnabledCheckbox, outlineEnabledCheckbox, pastelSlider) { |
572 | 612 | this.controlsContainer = controlsContainer; |
573 | 613 | this.playButton = playButton; |
574 | 614 | this.frameSlider = frameSlider; |
|
579 | 619 | this.lineWidthSlider = lineWidthSlider; |
580 | 620 | this.shadowEnabledCheckbox = shadowEnabledCheckbox; |
581 | 621 | this.outlineEnabledCheckbox = outlineEnabledCheckbox; |
| 622 | + this.pastelSlider = pastelSlider; |
582 | 623 |
|
583 | 624 | this.lineWidth = parseFloat(this.lineWidthSlider.value); // Read default from slider |
584 | 625 | this.autoRotate = this.rotationCheckbox.checked; // Read default from checkbox |
|
625 | 666 | this.render(); |
626 | 667 | }); |
627 | 668 | } |
| 669 | + |
| 670 | + if (this.pastelSlider) { |
| 671 | + this.pastelSlider.addEventListener('input', (e) => { |
| 672 | + this.pastelLevel = parseFloat(e.target.value); |
| 673 | + // Force-recalculate both color arrays |
| 674 | + this.colors = this._calculateSegmentColors(); |
| 675 | + this.plddtColors = this._calculatePlddtColors(); |
| 676 | + if (!this.isPlaying) { // Only render if not playing |
| 677 | + this.render(); |
| 678 | + } |
| 679 | + }); |
| 680 | + } |
628 | 681 |
|
629 | 682 | // Prevent canvas drag from interfering with slider |
630 | 683 | const handleSliderChange = (e) => { |
|
654 | 707 | // Also prevent canvas drag when interacting with other controls |
655 | 708 | const allControls = [this.playButton, this.trajectorySelect, this.speedSelect, |
656 | 709 | this.rotationCheckbox, this.lineWidthSlider, |
657 | | - this.shadowEnabledCheckbox, this.outlineEnabledCheckbox]; |
| 710 | + this.shadowEnabledCheckbox, this.outlineEnabledCheckbox, this.pastelSlider]; |
658 | 711 | allControls.forEach(control => { |
659 | 712 | if (control) { |
660 | 713 | control.addEventListener('mousedown', (e) => { |
|
810 | 863 | this.lineWidthSlider.disabled = !enabled; |
811 | 864 | if (this.shadowEnabledCheckbox) this.shadowEnabledCheckbox.disabled = !enabled; |
812 | 865 | if (this.outlineEnabledCheckbox) this.outlineEnabledCheckbox.disabled = !enabled; |
| 866 | + if (this.pastelSlider) this.pastelSlider.disabled = !enabled; |
813 | 867 | this.canvas.style.cursor = enabled ? 'grab' : 'wait'; |
814 | 868 | } |
815 | 869 |
|
|
1109 | 1163 | } |
1110 | 1164 | } |
1111 | 1165 |
|
1112 | | - const grey = {r: 128, g: 128, b: 128}; |
1113 | | - |
1114 | 1166 | return this.segmentIndices.map(segInfo => { |
| 1167 | + const grey = {r: 128, g: 128, b: 128}; |
| 1168 | + let color; |
1115 | 1169 | const i = segInfo.origIndex; |
1116 | 1170 | const type = segInfo.type; |
1117 | 1171 |
|
1118 | 1172 | if (type === 'L') { |
1119 | 1173 | // Ligands are grey unless in plddt mode |
1120 | | - return grey; |
| 1174 | + color = grey; |
1121 | 1175 | } |
1122 | 1176 | // plddt mode is handled in render() |
1123 | | - if (effectiveColorMode === 'chain') { |
| 1177 | + else if (effectiveColorMode === 'chain') { |
1124 | 1178 | const chainId = this.chains[i] || 'A'; |
1125 | 1179 | const chainIndex = chainIndexMap.get(chainId); |
1126 | | - return getChainColor(chainIndex !== undefined ? chainIndex : 0); |
| 1180 | + color = getChainColor(chainIndex !== undefined ? chainIndex : 0); |
1127 | 1181 | } |
1128 | 1182 | else { // rainbow |
1129 | 1183 | const scale = this.chainRainbowScales[segInfo.chainId]; |
1130 | | - if (scale) { return getRainbowColor(segInfo.colorIndex, scale.min, scale.max); } |
1131 | | - else { return grey; } |
| 1184 | + if (scale) { color = getRainbowColor(segInfo.colorIndex, scale.min, scale.max); } |
| 1185 | + else { color = grey; } |
1132 | 1186 | } |
| 1187 | + return this._applyPastel(color); |
1133 | 1188 | }); |
1134 | 1189 | } |
1135 | 1190 |
|
|
1138 | 1193 | const m = this.segmentIndices.length; |
1139 | 1194 | if (m === 0) return []; |
1140 | 1195 |
|
1141 | | - const grey = {r: 128, g: 128, b: 128}; |
1142 | 1196 | const colors = new Array(m); |
1143 | 1197 |
|
1144 | 1198 | for (let i = 0; i < m; i++) { |
1145 | 1199 | const segInfo = this.segmentIndices[i]; |
1146 | 1200 | const atomIdx = segInfo.origIndex; |
1147 | 1201 | const type = segInfo.type; |
| 1202 | + let color; |
1148 | 1203 |
|
1149 | 1204 | if (type === 'L') { |
1150 | 1205 | const plddt1 = (this.plddts[atomIdx] !== null && this.plddts[atomIdx] !== undefined) ? this.plddts[atomIdx] : 50; |
1151 | | - colors[i] = getPlddtColor(plddt1); |
| 1206 | + color = getPlddtColor(plddt1); |
1152 | 1207 | } else { |
1153 | 1208 | const plddt1 = (this.plddts[atomIdx] !== null && this.plddts[atomIdx] !== undefined) ? this.plddts[atomIdx] : 50; |
1154 | 1209 | const plddt2_idx = (segInfo.idx2 < this.coords.length) ? segInfo.idx2 : segInfo.idx1; |
1155 | 1210 | const plddt2 = (this.plddts[plddt2_idx] !== null && this.plddts[plddt2_idx] !== undefined) ? this.plddts[plddt2_idx] : 50; |
1156 | | - colors[i] = getPlddtColor((plddt1 + plddt2) / 2); |
| 1211 | + color = getPlddtColor((plddt1 + plddt2) / 2); |
1157 | 1212 | } |
| 1213 | + colors[i] = this._applyPastel(color); |
1158 | 1214 | } |
1159 | 1215 | return colors; |
1160 | 1216 | } |
|
1602 | 1658 | default_shadow: true, |
1603 | 1659 | default_outline: true, |
1604 | 1660 | default_width: 3.0, |
1605 | | - default_rotate: false, |
1606 | | - hide_controls: false, |
1607 | | - autoplay: false, |
1608 | | - hide_box: false |
1609 | | - }; |
| 1661 | + default_rotate: false, |
| 1662 | + hide_controls: false, |
| 1663 | + autoplay: false, |
| 1664 | + hide_box: false, |
| 1665 | + default_pastel: 0.25 |
| 1666 | + }; |
1610 | 1667 |
|
1611 | 1668 | // 2. Setup Canvas |
1612 | 1669 | const canvas = document.getElementById('canvas'); |
|
1645 | 1702 | const speedSelect = document.getElementById('speedSelect'); |
1646 | 1703 | const rotationCheckbox = document.getElementById('rotationCheckbox'); |
1647 | 1704 | const lineWidthSlider = document.getElementById('lineWidthSlider'); |
| 1705 | + const pastelSlider = document.getElementById('pastelSlider'); |
1648 | 1706 |
|
1649 | | - // Set defaults for width and rotate |
| 1707 | + // Set defaults for width, rotate, and pastel |
1650 | 1708 | lineWidthSlider.value = window.renderer.lineWidth; |
1651 | 1709 | rotationCheckbox.checked = window.renderer.autoRotate; |
| 1710 | + pastelSlider.value = window.renderer.pastelLevel; // Set default from renderer |
1652 | 1711 |
|
1653 | 1712 | // Pass ALL controls to the renderer |
1654 | 1713 | window.renderer.setUIControls( |
1655 | 1714 | controlsContainer, playButton, |
1656 | 1715 | frameSlider, frameCounter, trajectorySelect, |
1657 | 1716 | speedSelect, rotationCheckbox, lineWidthSlider, |
1658 | | - shadowEnabledCheckbox, outlineEnabledCheckbox |
| 1717 | + shadowEnabledCheckbox, outlineEnabledCheckbox, |
| 1718 | + pastelSlider |
1659 | 1719 | ); |
1660 | 1720 |
|
1661 | 1721 | // Handle new UI config options |
|
0 commit comments