Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Molecular Structure Viewer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: Arial, sans-serif; | |
| background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 25%, #a29bfe 50%, #74b9ff 75%, #81ecec 100%); | |
| color: #2d3436; | |
| overflow: hidden; | |
| } | |
| .container { | |
| display: flex; | |
| height: 100vh; | |
| } | |
| .viewer { | |
| flex: 1; | |
| position: relative; | |
| background: linear-gradient(135deg, rgba(255, 255, 255, 0.8), rgba(240, 240, 240, 0.8)); | |
| } | |
| #canvas3d { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .controls { | |
| width: 350px; | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| padding: 20px; | |
| overflow-y: auto; | |
| box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1); | |
| } | |
| .control-section { | |
| margin-bottom: 25px; | |
| padding: 15px; | |
| background: linear-gradient(135deg, rgba(162, 155, 254, 0.1), rgba(116, 185, 255, 0.1)); | |
| border-radius: 12px; | |
| border: 1px solid rgba(162, 155, 254, 0.2); | |
| } | |
| .control-section h3 { | |
| margin-bottom: 15px; | |
| color: #5f3dc4; | |
| font-size: 1.2em; | |
| font-weight: 600; | |
| } | |
| .input-group { | |
| margin-bottom: 15px; | |
| } | |
| .input-group label { | |
| display: block; | |
| margin-bottom: 5px; | |
| color: #5f3dc4; | |
| font-size: 0.9em; | |
| font-weight: 500; | |
| } | |
| .input-group input, | |
| .input-group select, | |
| .input-group textarea { | |
| width: 100%; | |
| padding: 10px; | |
| background: rgba(255, 255, 255, 0.8); | |
| border: 2px solid #dfe6e9; | |
| border-radius: 8px; | |
| color: #2d3436; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| .input-group input:focus, | |
| .input-group select:focus, | |
| .input-group textarea:focus { | |
| outline: none; | |
| border-color: #74b9ff; | |
| box-shadow: 0 0 0 3px rgba(116, 185, 255, 0.2); | |
| } | |
| button { | |
| width: 100%; | |
| padding: 12px; | |
| background: linear-gradient(135deg, #74b9ff, #a29bfe); | |
| border: none; | |
| border-radius: 8px; | |
| color: #fff; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| box-shadow: 0 4px 6px rgba(162, 155, 254, 0.3); | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 12px rgba(162, 155, 254, 0.4); | |
| } | |
| .view-buttons { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 10px; | |
| margin-top: 15px; | |
| } | |
| .view-buttons button { | |
| padding: 8px; | |
| font-size: 0.9em; | |
| } | |
| .info-panel { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| background: rgba(255, 255, 255, 0.9); | |
| backdrop-filter: blur(10px); | |
| padding: 20px; | |
| border-radius: 12px; | |
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); | |
| max-width: 300px; | |
| } | |
| .info-panel h4 { | |
| color: #5f3dc4; | |
| margin-bottom: 15px; | |
| font-weight: 600; | |
| } | |
| .info-item { | |
| margin: 8px 0; | |
| font-size: 0.95em; | |
| color: #2d3436; | |
| } | |
| .info-item span { | |
| color: #74b9ff; | |
| font-weight: 500; | |
| } | |
| .legend { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 20px; | |
| background: rgba(255, 255, 255, 0.9); | |
| backdrop-filter: blur(10px); | |
| padding: 20px; | |
| border-radius: 12px; | |
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); | |
| } | |
| .legend h4 { | |
| color: #5f3dc4; | |
| margin-bottom: 10px; | |
| font-weight: 600; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| margin: 8px 0; | |
| color: #2d3436; | |
| } | |
| .legend-color { | |
| width: 20px; | |
| height: 20px; | |
| margin-right: 10px; | |
| border-radius: 3px; | |
| } | |
| .loading { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| text-align: center; | |
| } | |
| .loading-spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 3px solid #333; | |
| border-top-color: #00ff88; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .slider { | |
| flex: 1; | |
| -webkit-appearance: none; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: linear-gradient(to right, #dfe6e9 0%, #74b9ff 50%, #a29bfe 100%); | |
| outline: none; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: #5f3dc4; | |
| cursor: pointer; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| .slider::-moz-range-thumb { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: #5f3dc4; | |
| cursor: pointer; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); | |
| } | |
| .slider-value { | |
| min-width: 40px; | |
| text-align: right; | |
| color: #5f3dc4; | |
| font-weight: 500; | |
| } | |
| #analysisResults { | |
| background: rgba(255, 255, 255, 0.6); | |
| padding: 15px; | |
| border-radius: 8px; | |
| color: #2d3436; | |
| } | |
| #analysisResults strong { | |
| color: #5f3dc4; | |
| } | |
| /* Molecule info tooltip */ | |
| .tooltip { | |
| position: absolute; | |
| background: rgba(0, 0, 0, 0.9); | |
| padding: 10px; | |
| border-radius: 4px; | |
| border: 1px solid #00ff88; | |
| pointer-events: none; | |
| z-index: 1000; | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="viewer"> | |
| <canvas id="canvas3d"></canvas> | |
| <div class="loading" id="loading"> | |
| <div class="loading-spinner"></div> | |
| <p>Initializing 3D viewer...</p> | |
| </div> | |
| <div class="info-panel" id="infoPanel"> | |
| <h4>Molecule Information</h4> | |
| <div class="info-item">Type: <span id="moleculeType">-</span></div> | |
| <div class="info-item">Length: <span id="moleculeLength">-</span></div> | |
| <div class="info-item">Atoms: <span id="atomCount">-</span></div> | |
| <div class="info-item">Mode: <span id="viewMode">-</span></div> | |
| </div> | |
| <div class="legend" id="legend" style="display: none;"> | |
| <h4>Color Legend</h4> | |
| <div id="legendItems"></div> | |
| </div> | |
| <div class="tooltip" id="tooltip"></div> | |
| </div> | |
| <div class="controls"> | |
| <h2 style=" | |
| margin-bottom: 20px; | |
| text-align: center; | |
| background: linear-gradient(135deg, #5f3dc4, #74b9ff, #fd79a8); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 1.8em; | |
| ">🧬 3D Structure Controls</h2> | |
| <div class="control-section"> | |
| <h3>🧬 Input Sequence</h3> | |
| <div class="input-group"> | |
| <label>Molecule Type</label> | |
| <select id="moleculeTypeSelect"> | |
| <option value="dna">DNA</option> | |
| <option value="protein">Protein</option> | |
| <option value="rna">RNA</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label>Sequence</label> | |
| <textarea id="sequenceInput" placeholder="Enter DNA sequence (e.g., ATCGATCGATCG) or protein sequence (e.g., MKTAYIAKQRQISFVKSHFSRQ)">ATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCG</textarea> | |
| </div> | |
| <button onclick="generateStructure()">Generate 3D Structure</button> | |
| </div> | |
| <div class="control-section"> | |
| <h3>🎨 Visualization Mode</h3> | |
| <div class="view-buttons"> | |
| <button onclick="setViewMode('cartoon')">Cartoon</button> | |
| <button onclick="setViewMode('stick')">Stick</button> | |
| <button onclick="setViewMode('sphere')">Sphere</button> | |
| <button onclick="setViewMode('surface')">Surface</button> | |
| </div> | |
| <div class="input-group" style="margin-top: 15px;"> | |
| <label>Color Scheme</label> | |
| <select id="colorScheme" onchange="updateColorScheme()"> | |
| <option value="element">By Element</option> | |
| <option value="residue">By Residue</option> | |
| <option value="chain">By Chain</option> | |
| <option value="hydrophobicity">By Hydrophobicity</option> | |
| <option value="charge">By Charge</option> | |
| <option value="secondary">By Secondary Structure</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>⚙️ Display Options</h3> | |
| <div class="input-group"> | |
| <label>Atom Size</label> | |
| <div class="slider-container"> | |
| <input type="range" id="atomSize" class="slider" min="0.5" max="3" step="0.1" value="1" onchange="updateAtomSize()"> | |
| <span class="slider-value" id="atomSizeValue">1.0</span> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label>Bond Thickness</label> | |
| <div class="slider-container"> | |
| <input type="range" id="bondThickness" class="slider" min="0.1" max="1" step="0.1" value="0.3" onchange="updateBondThickness()"> | |
| <span class="slider-value" id="bondThicknessValue">0.3</span> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label> | |
| <input type="checkbox" id="showHydrogens" onchange="toggleHydrogens()"> Show Hydrogens | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label> | |
| <input type="checkbox" id="showLabels" onchange="toggleLabels()"> Show Atom Labels | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label> | |
| <input type="checkbox" id="autoRotate" checked onchange="toggleRotation()"> Auto Rotate | |
| </label> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>📊 Analysis</h3> | |
| <button onclick="analyzeStructure()">Analyze Structure</button> | |
| <div id="analysisResults" style="margin-top: 15px; font-size: 0.9em;"></div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>💾 Export</h3> | |
| <button onclick="exportStructure('pdb')">Export as PDB</button> | |
| <button onclick="exportStructure('image')" style="margin-top: 10px;">Export as Image</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // Three.js scene setup | |
| let scene, camera, renderer, controls; | |
| let moleculeGroup; | |
| let currentMode = 'cartoon'; | |
| let autoRotate = true; | |
| let raycaster, mouse; | |
| let INTERSECTED; | |
| // Molecular data | |
| let currentSequence = ''; | |
| let currentType = 'dna'; | |
| let atoms = []; | |
| let bonds = []; | |
| // Color schemes - Pastel colors | |
| const elementColors = { | |
| 'C': 0x9b9b9b, // Soft gray | |
| 'H': 0xf8f8f8, // Almost white | |
| 'N': 0x74b9ff, // Soft blue | |
| 'O': 0xfd79a8, // Soft pink | |
| 'P': 0xfdcb6e, // Soft orange | |
| 'S': 0xf9ca24 // Soft yellow | |
| }; | |
| const residueColors = { | |
| 'A': 0xfd79a8, // Soft pink | |
| 'T': 0x81ecec, // Soft turquoise | |
| 'G': 0xfdcb6e, // Soft yellow | |
| 'C': 0xa29bfe, // Soft purple | |
| 'U': 0xf8b5ff, // Soft magenta | |
| // Amino acids - all in pastel shades | |
| 'ALA': 0xc7ecee, 'ARG': 0xdfe6e9, 'ASN': 0xfab1a0, 'ASP': 0xfd79a8, | |
| 'CYS': 0xffeaa7, 'GLN': 0xff7675, 'GLU': 0xe17055, 'GLY': 0xffffff, | |
| 'HIS': 0xa29bfe, 'ILE': 0x81ecec, 'LEU': 0x74b9ff, 'LYS': 0x686de0, | |
| 'MET': 0xfdcb6e, 'PHE': 0x6c5ce7, 'PRO': 0xb2bec3, 'SER': 0xff7675, | |
| 'THR': 0xf0932b, 'TRP': 0x5f3dc4, 'TYR': 0xeb4d4b, 'VAL': 0xbadc58 | |
| }; | |
| function init() { | |
| // Scene setup | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0xffffff); | |
| scene.fog = new THREE.Fog(0xffffff, 100, 200); | |
| // Camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 0, 50); | |
| // Renderer | |
| renderer = new THREE.WebGLRenderer({ | |
| canvas: document.getElementById('canvas3d'), | |
| antialias: true, | |
| alpha: true | |
| }); | |
| renderer.setSize(window.innerWidth - 350, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.setClearColor(0xffffff, 0.9); | |
| // Lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); | |
| scene.add(ambientLight); | |
| const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.6); | |
| directionalLight1.position.set(50, 50, 50); | |
| directionalLight1.castShadow = true; | |
| scene.add(directionalLight1); | |
| const directionalLight2 = new THREE.DirectionalLight(0xa29bfe, 0.3); | |
| directionalLight2.position.set(-50, 50, -50); | |
| scene.add(directionalLight2); | |
| // Controls | |
| controls = { | |
| rotateX: 0, | |
| rotateY: 0.01, | |
| zoom: 1 | |
| }; | |
| // Raycaster for mouse interaction | |
| raycaster = new THREE.Raycaster(); | |
| mouse = new THREE.Vector2(); | |
| // Mouse events | |
| renderer.domElement.addEventListener('mousemove', onMouseMove); | |
| renderer.domElement.addEventListener('wheel', onMouseWheel); | |
| renderer.domElement.addEventListener('mousedown', onMouseDown); | |
| renderer.domElement.addEventListener('mouseup', onMouseUp); | |
| // Initialize molecule group | |
| moleculeGroup = new THREE.Group(); | |
| scene.add(moleculeGroup); | |
| // Hide loading | |
| document.getElementById('loading').style.display = 'none'; | |
| // Start animation loop | |
| animate(); | |
| // Generate initial structure | |
| generateStructure(); | |
| } | |
| function generateStructure() { | |
| const sequence = document.getElementById('sequenceInput').value.trim().toUpperCase(); | |
| const type = document.getElementById('moleculeTypeSelect').value; | |
| if (!sequence) { | |
| alert('Please enter a sequence'); | |
| return; | |
| } | |
| currentSequence = sequence; | |
| currentType = type; | |
| // Clear existing structure | |
| moleculeGroup.clear(); | |
| atoms = []; | |
| bonds = []; | |
| // Generate structure based on type | |
| if (type === 'dna') { | |
| generateDNAStructure(sequence); | |
| } else if (type === 'protein') { | |
| generateProteinStructure(sequence); | |
| } else if (type === 'rna') { | |
| generateRNAStructure(sequence); | |
| } | |
| // Update info panel | |
| updateInfoPanel(); | |
| } | |
| function generateDNAStructure(sequence) { | |
| const radius = 10; | |
| const rise = 3.4; | |
| const basesPerTurn = 10; | |
| const anglePerBase = (2 * Math.PI) / basesPerTurn; | |
| // Generate double helix | |
| for (let i = 0; i < sequence.length; i++) { | |
| const angle = i * anglePerBase; | |
| const height = i * rise / basesPerTurn; | |
| const base = sequence[i]; | |
| // Strand 1 | |
| const x1 = radius * Math.cos(angle); | |
| const z1 = radius * Math.sin(angle); | |
| const y1 = height; | |
| // Strand 2 (complementary) | |
| const x2 = radius * Math.cos(angle + Math.PI); | |
| const z2 = radius * Math.sin(angle + Math.PI); | |
| const y2 = height; | |
| // Add sugar-phosphate backbone | |
| addAtom(x1, y1, z1, 'P', i, 'strand1'); | |
| addAtom(x2, y2, z2, 'P', i, 'strand2'); | |
| // Add bases | |
| const baseX1 = x1 * 0.5; | |
| const baseZ1 = z1 * 0.5; | |
| const baseX2 = x2 * 0.5; | |
| const baseZ2 = z2 * 0.5; | |
| addAtom(baseX1, y1, baseZ1, base, i, 'base1'); | |
| addAtom(baseX2, y2, baseZ2, getComplementaryBase(base), i, 'base2'); | |
| // Add hydrogen bonds between base pairs | |
| addBond(atoms.length - 2, atoms.length - 1, 'hydrogen'); | |
| // Connect backbone | |
| if (i > 0) { | |
| addBond(atoms.length - 4, atoms.length - 8, 'covalent'); | |
| addBond(atoms.length - 3, atoms.length - 7, 'covalent'); | |
| } | |
| // Connect base to backbone | |
| addBond(atoms.length - 4, atoms.length - 2, 'covalent'); | |
| addBond(atoms.length - 3, atoms.length - 1, 'covalent'); | |
| } | |
| renderMolecule(); | |
| } | |
| function generateProteinStructure(sequence) { | |
| // Simplified protein structure generation | |
| // In reality, this would require complex folding algorithms | |
| let x = 0, y = 0, z = 0; | |
| let prevCarbonIndex = -1; | |
| // Generate a simple helix structure | |
| const helixRadius = 5; | |
| const helixPitch = 5.4; // Angstroms per turn | |
| const residuesPerTurn = 3.6; | |
| for (let i = 0; i < sequence.length; i++) { | |
| const residue = sequence[i]; | |
| const angle = (i / residuesPerTurn) * 2 * Math.PI; | |
| const height = (i / residuesPerTurn) * helixPitch; | |
| // Backbone atoms | |
| x = helixRadius * Math.cos(angle); | |
| z = helixRadius * Math.sin(angle); | |
| y = height; | |
| // Add backbone atoms (N, CA, C) | |
| const nIndex = atoms.length; | |
| addAtom(x - 0.5, y, z, 'N', i, residue); | |
| const caIndex = atoms.length; | |
| addAtom(x, y, z, 'C', i, residue); | |
| const cIndex = atoms.length; | |
| addAtom(x + 0.5, y, z, 'C', i, residue); | |
| // Add side chain (simplified) | |
| const sideX = x + 2 * Math.cos(angle + Math.PI/2); | |
| const sideZ = z + 2 * Math.sin(angle + Math.PI/2); | |
| addAtom(sideX, y, sideZ, 'C', i, residue); | |
| // Connect backbone | |
| addBond(nIndex, caIndex, 'covalent'); | |
| addBond(caIndex, cIndex, 'covalent'); | |
| addBond(caIndex, atoms.length - 1, 'covalent'); | |
| // Connect to previous residue | |
| if (prevCarbonIndex >= 0) { | |
| addBond(prevCarbonIndex, nIndex, 'covalent'); | |
| } | |
| prevCarbonIndex = cIndex; | |
| } | |
| renderMolecule(); | |
| } | |
| function generateRNAStructure(sequence) { | |
| // Similar to DNA but single-stranded with different sugar | |
| const radius = 8; | |
| const rise = 5.9; | |
| const basesPerTurn = 11; | |
| const anglePerBase = (2 * Math.PI) / basesPerTurn; | |
| for (let i = 0; i < sequence.length; i++) { | |
| const angle = i * anglePerBase; | |
| const height = i * rise / basesPerTurn; | |
| const base = sequence[i] === 'T' ? 'U' : sequence[i]; | |
| const x = radius * Math.cos(angle); | |
| const z = radius * Math.sin(angle); | |
| const y = height; | |
| // Add sugar-phosphate backbone | |
| addAtom(x, y, z, 'P', i, 'backbone'); | |
| // Add base | |
| const baseX = x * 0.6; | |
| const baseZ = z * 0.6; | |
| addAtom(baseX, y, baseZ, base, i, 'base'); | |
| // Connect backbone | |
| if (i > 0) { | |
| addBond(atoms.length - 2, atoms.length - 4, 'covalent'); | |
| } | |
| // Connect base to backbone | |
| addBond(atoms.length - 2, atoms.length - 1, 'covalent'); | |
| } | |
| renderMolecule(); | |
| } | |
| function addAtom(x, y, z, element, residueIndex, type) { | |
| atoms.push({ | |
| position: new THREE.Vector3(x, y, z), | |
| element: element, | |
| residueIndex: residueIndex, | |
| type: type | |
| }); | |
| } | |
| function addBond(atom1Index, atom2Index, type) { | |
| if (atom1Index >= 0 && atom2Index >= 0 && atom1Index < atoms.length && atom2Index < atoms.length) { | |
| bonds.push({ | |
| atom1: atom1Index, | |
| atom2: atom2Index, | |
| type: type | |
| }); | |
| } | |
| } | |
| function renderMolecule() { | |
| moleculeGroup.clear(); | |
| if (currentMode === 'cartoon') { | |
| renderCartoon(); | |
| } else if (currentMode === 'stick') { | |
| renderStick(); | |
| } else if (currentMode === 'sphere') { | |
| renderSphere(); | |
| } else if (currentMode === 'surface') { | |
| renderSurface(); | |
| } | |
| // Center the molecule | |
| const box = new THREE.Box3().setFromObject(moleculeGroup); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| moleculeGroup.position.sub(center); | |
| } | |
| function renderCartoon() { | |
| if (currentType === 'dna' || currentType === 'rna') { | |
| // Render as ribbon | |
| const curve = new THREE.CatmullRomCurve3( | |
| atoms.filter(a => a.type.includes('backbone')).map(a => a.position) | |
| ); | |
| const tubeGeometry = new THREE.TubeGeometry(curve, 100, 1, 8, false); | |
| const tubeMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x74b9ff, | |
| shininess: 100, | |
| specular: 0xffffff, | |
| emissive: 0x74b9ff, | |
| emissiveIntensity: 0.1 | |
| }); | |
| const tube = new THREE.Mesh(tubeGeometry, tubeMaterial); | |
| moleculeGroup.add(tube); | |
| // Add base representations | |
| atoms.filter(a => a.type.includes('base')).forEach(atom => { | |
| const geometry = new THREE.BoxGeometry(3, 0.5, 1); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: residueColors[atom.element] || 0xa29bfe, | |
| shininess: 100, | |
| specular: 0xffffff | |
| }); | |
| const base = new THREE.Mesh(geometry, material); | |
| base.position.copy(atom.position); | |
| // Orient towards center | |
| base.lookAt(new THREE.Vector3(0, atom.position.y, 0)); | |
| moleculeGroup.add(base); | |
| }); | |
| } else if (currentType === 'protein') { | |
| // Render as ribbon with secondary structure | |
| renderProteinCartoon(); | |
| } | |
| } | |
| function renderProteinCartoon() { | |
| // Create ribbon through CA atoms | |
| const caAtoms = atoms.filter(a => a.type !== 'N' && a.type !== 'C'); | |
| if (caAtoms.length < 2) return; | |
| const curve = new THREE.CatmullRomCurve3(caAtoms.map(a => a.position)); | |
| const tubeGeometry = new THREE.TubeGeometry(curve, caAtoms.length * 10, 1.5, 4, false); | |
| // Color by secondary structure prediction (simplified) | |
| const tubeMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xfd79a8, | |
| shininess: 100, | |
| specular: 0xffffff, | |
| emissive: 0xfd79a8, | |
| emissiveIntensity: 0.1 | |
| }); | |
| const tube = new THREE.Mesh(tubeGeometry, tubeMaterial); | |
| moleculeGroup.add(tube); | |
| } | |
| function renderStick() { | |
| // Render atoms as small spheres | |
| atoms.forEach((atom, index) => { | |
| const geometry = new THREE.SphereGeometry(0.5, 16, 16); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: getAtomColor(atom), | |
| shininess: 100, | |
| specular: 0xffffff | |
| }); | |
| const sphere = new THREE.Mesh(geometry, material); | |
| sphere.position.copy(atom.position); | |
| sphere.userData = { atomIndex: index, atom: atom }; | |
| moleculeGroup.add(sphere); | |
| }); | |
| // Render bonds as cylinders | |
| bonds.forEach(bond => { | |
| const atom1 = atoms[bond.atom1]; | |
| const atom2 = atoms[bond.atom2]; | |
| const direction = new THREE.Vector3().subVectors(atom2.position, atom1.position); | |
| const distance = direction.length(); | |
| direction.normalize(); | |
| const geometry = new THREE.CylinderGeometry(0.2, 0.2, distance, 8); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: bond.type === 'hydrogen' ? 0xdfe6e9 : 0xb2bec3, | |
| shininess: 50 | |
| }); | |
| const cylinder = new THREE.Mesh(geometry, material); | |
| cylinder.position.copy(atom1.position).add(direction.multiplyScalar(distance / 2)); | |
| cylinder.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); | |
| moleculeGroup.add(cylinder); | |
| }); | |
| } | |
| function renderSphere() { | |
| atoms.forEach((atom, index) => { | |
| const radius = getAtomRadius(atom.element); | |
| const geometry = new THREE.SphereGeometry(radius, 32, 32); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: getAtomColor(atom), | |
| shininess: 100, | |
| specular: 0x222222 | |
| }); | |
| const sphere = new THREE.Mesh(geometry, material); | |
| sphere.position.copy(atom.position); | |
| sphere.userData = { atomIndex: index, atom: atom }; | |
| moleculeGroup.add(sphere); | |
| }); | |
| } | |
| function renderSurface() { | |
| // Simplified surface representation | |
| const points = atoms.map(a => a.position); | |
| const geometry = new THREE.ConvexGeometry(points); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: 0xa29bfe, | |
| transparent: true, | |
| opacity: 0.6, | |
| side: THREE.DoubleSide, | |
| shininess: 100, | |
| specular: 0xffffff | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| moleculeGroup.add(mesh); | |
| } | |
| function getAtomColor(atom) { | |
| const scheme = document.getElementById('colorScheme').value; | |
| if (scheme === 'element') { | |
| return elementColors[atom.element] || 0xb2bec3; | |
| } else if (scheme === 'residue') { | |
| return residueColors[atom.element] || residueColors[atom.type] || 0xb2bec3; | |
| } else if (scheme === 'chain') { | |
| return atom.type.includes('strand1') || atom.type === 'backbone' ? 0xfd79a8 : 0x74b9ff; | |
| } else if (scheme === 'hydrophobicity') { | |
| // Simplified hydrophobicity scale with pastel colors | |
| const hydrophobic = ['A', 'V', 'I', 'L', 'M', 'F', 'W']; | |
| return hydrophobic.includes(atom.element) ? 0xfdcb6e : 0x81ecec; | |
| } else if (scheme === 'charge') { | |
| const positive = ['R', 'K', 'H']; | |
| const negative = ['D', 'E']; | |
| if (positive.includes(atom.element)) return 0x74b9ff; | |
| if (negative.includes(atom.element)) return 0xfd79a8; | |
| return 0xdfe6e9; | |
| } | |
| return 0xb2bec3; | |
| } | |
| function getAtomRadius(element) { | |
| const radii = { | |
| 'H': 1.2, 'C': 1.7, 'N': 1.55, 'O': 1.52, 'P': 1.8, 'S': 1.8, | |
| 'A': 2.0, 'T': 2.0, 'G': 2.0, 'C': 2.0, 'U': 2.0 | |
| }; | |
| return radii[element] || 1.5; | |
| } | |
| function getComplementaryBase(base) { | |
| const complements = { 'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G' }; | |
| return complements[base] || 'N'; | |
| } | |
| function setViewMode(mode) { | |
| currentMode = mode; | |
| renderMolecule(); | |
| document.getElementById('viewMode').textContent = mode.charAt(0).toUpperCase() + mode.slice(1); | |
| } | |
| function updateColorScheme() { | |
| renderMolecule(); | |
| updateLegend(); | |
| } | |
| function updateLegend() { | |
| const scheme = document.getElementById('colorScheme').value; | |
| const legend = document.getElementById('legend'); | |
| const legendItems = document.getElementById('legendItems'); | |
| legendItems.innerHTML = ''; | |
| if (scheme === 'element') { | |
| Object.entries(elementColors).forEach(([element, color]) => { | |
| addLegendItem(element, color); | |
| }); | |
| } else if (scheme === 'residue' && currentType === 'dna') { | |
| ['A', 'T', 'G', 'C'].forEach(base => { | |
| addLegendItem(base, residueColors[base]); | |
| }); | |
| } | |
| legend.style.display = legendItems.children.length > 0 ? 'block' : 'none'; | |
| } | |
| function addLegendItem(label, color) { | |
| const item = document.createElement('div'); | |
| item.className = 'legend-item'; | |
| item.innerHTML = ` | |
| <div class="legend-color" style="background: #${color.toString(16).padStart(6, '0')}"></div> | |
| <span>${label}</span> | |
| `; | |
| document.getElementById('legendItems').appendChild(item); | |
| } | |
| function updateAtomSize() { | |
| const value = document.getElementById('atomSize').value; | |
| document.getElementById('atomSizeValue').textContent = value; | |
| // Re-render with new size | |
| renderMolecule(); | |
| } | |
| function updateBondThickness() { | |
| const value = document.getElementById('bondThickness').value; | |
| document.getElementById('bondThicknessValue').textContent = value; | |
| // Re-render with new thickness | |
| renderMolecule(); | |
| } | |
| function toggleHydrogens() { | |
| // In a real implementation, this would show/hide hydrogen atoms | |
| renderMolecule(); | |
| } | |
| function toggleLabels() { | |
| // Toggle atom labels | |
| renderMolecule(); | |
| } | |
| function toggleRotation() { | |
| autoRotate = document.getElementById('autoRotate').checked; | |
| } | |
| function updateInfoPanel() { | |
| document.getElementById('moleculeType').textContent = currentType.toUpperCase(); | |
| document.getElementById('moleculeLength').textContent = currentSequence.length; | |
| document.getElementById('atomCount').textContent = atoms.length; | |
| document.getElementById('viewMode').textContent = currentMode.charAt(0).toUpperCase() + currentMode.slice(1); | |
| } | |
| function analyzeStructure() { | |
| const results = document.getElementById('analysisResults'); | |
| let analysis = '<strong>Structure Analysis:</strong><br>'; | |
| if (currentType === 'dna') { | |
| const gc = (currentSequence.match(/[GC]/g) || []).length / currentSequence.length * 100; | |
| analysis += `GC Content: ${gc.toFixed(1)}%<br>`; | |
| analysis += `Length: ${currentSequence.length} bp<br>`; | |
| analysis += `Estimated MW: ${(currentSequence.length * 330).toLocaleString()} Da<br>`; | |
| analysis += `Melting Temp: ${(81.5 + 0.41 * gc - 675 / currentSequence.length).toFixed(1)}°C<br>`; | |
| } else if (currentType === 'protein') { | |
| analysis += `Length: ${currentSequence.length} aa<br>`; | |
| analysis += `Estimated MW: ${(currentSequence.length * 110).toLocaleString()} Da<br>`; | |
| // Count charged residues | |
| const positive = (currentSequence.match(/[RKH]/g) || []).length; | |
| const negative = (currentSequence.match(/[DE]/g) || []).length; | |
| analysis += `Charged residues: +${positive}/-${negative}<br>`; | |
| // Hydrophobicity | |
| const hydrophobic = (currentSequence.match(/[AVILMFW]/g) || []).length; | |
| analysis += `Hydrophobic: ${(hydrophobic / currentSequence.length * 100).toFixed(1)}%<br>`; | |
| } | |
| results.innerHTML = analysis; | |
| } | |
| function exportStructure(format) { | |
| if (format === 'pdb') { | |
| let pdbContent = 'REMARK Generated by 3D Molecular Viewer\n'; | |
| atoms.forEach((atom, i) => { | |
| pdbContent += `ATOM ${(i+1).toString().padStart(5)} ${atom.element.padEnd(4)} RES 1 ${atom.position.x.toFixed(3).padStart(8)}${atom.position.y.toFixed(3).padStart(8)}${atom.position.z.toFixed(3).padStart(8)} 1.00 0.00 ${atom.element}\n`; | |
| }); | |
| pdbContent += 'END\n'; | |
| downloadFile('structure.pdb', pdbContent); | |
| } else if (format === 'image') { | |
| renderer.render(scene, camera); | |
| renderer.domElement.toBlob(blob => { | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'structure.png'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| } | |
| } | |
| function downloadFile(filename, content) { | |
| const blob = new Blob([content], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Mouse interaction handlers | |
| let isDragging = false; | |
| let previousMousePosition = { x: 0, y: 0 }; | |
| function onMouseMove(event) { | |
| if (isDragging) { | |
| const deltaMove = { | |
| x: event.clientX - previousMousePosition.x, | |
| y: event.clientY - previousMousePosition.y | |
| }; | |
| moleculeGroup.rotation.y += deltaMove.x * 0.01; | |
| moleculeGroup.rotation.x += deltaMove.y * 0.01; | |
| previousMousePosition = { | |
| x: event.clientX, | |
| y: event.clientY | |
| }; | |
| } else { | |
| // Hover detection | |
| mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1; | |
| raycaster.setFromCamera(mouse, camera); | |
| const intersects = raycaster.intersectObjects(moleculeGroup.children); | |
| if (intersects.length > 0) { | |
| if (INTERSECTED !== intersects[0].object) { | |
| if (INTERSECTED) INTERSECTED.material.emissive = new THREE.Color(0x000000); | |
| INTERSECTED = intersects[0].object; | |
| INTERSECTED.material.emissive = new THREE.Color(0x444444); | |
| // Show tooltip | |
| if (INTERSECTED.userData.atom) { | |
| const tooltip = document.getElementById('tooltip'); | |
| tooltip.style.display = 'block'; | |
| tooltip.style.left = event.clientX + 10 + 'px'; | |
| tooltip.style.top = event.clientY + 10 + 'px'; | |
| tooltip.innerHTML = ` | |
| Element: ${INTERSECTED.userData.atom.element}<br> | |
| Type: ${INTERSECTED.userData.atom.type}<br> | |
| Index: ${INTERSECTED.userData.atomIndex} | |
| `; | |
| } | |
| } | |
| } else { | |
| if (INTERSECTED) { | |
| INTERSECTED.material.emissive = new THREE.Color(0x000000); | |
| INTERSECTED = null; | |
| document.getElementById('tooltip').style.display = 'none'; | |
| } | |
| } | |
| } | |
| } | |
| function onMouseDown(event) { | |
| isDragging = true; | |
| previousMousePosition = { | |
| x: event.clientX, | |
| y: event.clientY | |
| }; | |
| } | |
| function onMouseUp() { | |
| isDragging = false; | |
| } | |
| function onMouseWheel(event) { | |
| camera.position.z += event.deltaY * 0.1; | |
| camera.position.z = Math.max(10, Math.min(200, camera.position.z)); | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (autoRotate && !isDragging) { | |
| moleculeGroup.rotation.y += 0.005; | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = (window.innerWidth - 350) / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth - 350, window.innerHeight); | |
| }); | |
| // Initialize on load | |
| window.addEventListener('load', init); | |
| // Custom ConvexGeometry implementation (simplified) | |
| THREE.ConvexGeometry = function(points) { | |
| THREE.Geometry.call(this); | |
| // Create a simple convex hull (very simplified) | |
| const geometry = new THREE.Geometry(); | |
| geometry.vertices = points; | |
| // Create faces (simplified triangulation) | |
| if (points.length >= 3) { | |
| for (let i = 0; i < points.length - 2; i++) { | |
| geometry.faces.push(new THREE.Face3(0, i + 1, i + 2)); | |
| } | |
| } | |
| geometry.computeFaceNormals(); | |
| geometry.computeVertexNormals(); | |
| return new THREE.BufferGeometry().fromGeometry(geometry); | |
| }; | |
| </script> | |
| </body> | |
| </html> |