DNA-Diffusion / dna-slot-machine.html
openfree's picture
Update dna-slot-machine.html
44873a8 verified
<!DOCTYPE html>
<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>