Spaces:
Running
on
Zero
Running
on
Zero
<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> |