<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Interactive 3D DNA Simulator</title>
   <script src="https://cdn.tailwindcss.com"></script>
   <script type="importmap">
       {
           "imports": {
               "three": "https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.module.js",
               "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/"
           }
       }
   </script>
   <style>
       body { background: radial-gradient(circle at center, #0f172a, #000); font-family: 'Inter', sans-serif; }
       #simulation-container canvas { cursor: pointer; }
       .base-button.selected { outline: 2px solid #6366F1; transform: scale(1.05); }
   </style>
</head>
<body class="text-gray-200 flex flex-col items-center justify-center min-h-screen p-4">

   <div class="flex flex-col items-center">
       <div id="simulation-container" class="w-[1000px] h-[1000px] bg-black rounded-t-2xl shadow-2xl overflow-hidden relative border-2 border-gray-700"></div>
       <div id="ui-container" class="w-[1000px] h-[300px] rounded-b-2xl shadow-2xl p-6 flex flex-col justify-between bg-gray-800/80">
           <div class="flex justify-between items-start">
               <div>
                   <h1 class="text-3xl font-extrabold text-white">Interactive DNA Strand Simulator</h1>
                   <p class="text-gray-400">Select a preset or click on a base pair to edit the sequence.</p>
               </div>
               <div>
                   <label for="sample-select" class="text-sm text-gray-400 block mb-1">Load DNA Sample</label>
                   <select id="sample-select" class="bg-gray-700 border border-gray-600 rounded-md p-2 w-72"></select>
               </div>
           </div>
           <div id="editor-panel" class="flex-grow flex items-center justify-center gap-8 bg-gray-900/50 rounded-lg p-4 border border-gray-700">
               <div class="text-center">
                   <p class="text-gray-400 text-sm">Selected Pair Index</p>
                   <p id="selected-index" class="text-3xl fira-code">-</p>
               </div>
               <div class="w-px h-16 bg-gray-600"></div>
               <div class="text-center">
                   <p class="text-gray-400 text-sm">Current Strand</p>
                   <p id="selected-base" class="text-5xl fira-code font-bold">-</p>
               </div>
               <div class="text-center">
                   <p class="text-gray-400 text-sm">Complementary Strand</p>
                   <p id="complementary-base" class="text-5xl fira-code font-bold">-</p>
               </div>
               <div class="w-px h-16 bg-gray-600"></div>
               <div id="base-editor" class="text-center space-y-2 opacity-25 transition-opacity">
                   <p class="text-gray-400 text-sm">Change Base To</p>
                   <div id="base-buttons" class="flex gap-2"></div>
               </div>
           </div>
       </div>
   </div>

   <script type="module">
       import * as THREE from 'three';
       import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

       let scene, camera, renderer, controls, dnaGroup;
       let dnaSequence = '';
       let interactiveObjects = [];
       let selectedPair = null;

       const BASE_PAIRS = { 'A': 'T', 'T': 'A', 'C': 'G', 'G': 'C' };
       const BASE_COLORS = { 'A': 0x0077ff, 'T': 0x00ff00, 'C': 0xffff00, 'G': 0xff0000 };
       const dnaSamples = {
           'sickle-cell': { name: 'Sickle Cell Anemia (HBB Gene)', sequence: 'CCTGTGGAG' },
           'normal-hemoglobin': { name: 'Normal Hemoglobin (HBB Gene)', sequence: 'CCTGAGGAG' },
           'cystic-fibrosis': { name: 'Cystic Fibrosis (CFTR ΔF508)', sequence: 'ATCATCGGTGTT' }
       };

       const simContainer = document.getElementById('simulation-container');
       const sampleSelect = document.getElementById('sample-select');
       const buildButton = document.getElementById('build-button');
       const selectedIndex = document.getElementById('selected-index');
       const selectedBase = document.getElementById('selected-base');
       const complementaryBase = document.getElementById('complementary-base');
       const baseEditor = document.getElementById('base-editor');
       const baseButtons = document.getElementById('base-buttons');

       function initialize() {
           init3D();
           setupUI();
           loadSample('sickle-cell');
           buildDNA();
           animate3D();
       }

       function setupUI() {
           for (const key in dnaSamples) {
               const option = document.createElement('option');
               option.value = key;
               option.textContent = dnaSamples[key].name;
               sampleSelect.appendChild(option);
           }
           sampleSelect.value = 'sickle-cell';
           sampleSelect.addEventListener('change', () => loadSample(sampleSelect.value));
           ['A', 'T', 'C', 'G'].forEach(base => {
               const button = document.createElement('button');
               button.textContent = base;
               button.dataset.base = base;
               button.className = `base-button w-10 h-10 rounded-full font-bold`;
               button.style.backgroundColor = `#${BASE_COLORS[base].toString(16).padStart(6, '0')}`;
               button.style.color = (base === 'C' || base === 'G') ? '#333' : '#fff';
               button.addEventListener('click', () => handleBaseChange(base));
               baseButtons.appendChild(button);
           });
           simContainer.addEventListener('click', onCanvasClick);
       }

       function loadSample(sampleKey) {
           if (dnaSamples[sampleKey]) {
               dnaSequence = dnaSamples[sampleKey].sequence;
               buildDNA();
           }
       }

       function updateEditorPanel() {
           if (selectedPair) {
               baseEditor.classList.remove('opacity-25');
               selectedIndex.textContent = selectedPair.index;
               selectedBase.textContent = selectedPair.base;
               complementaryBase.textContent = selectedPair.pair;
               document.querySelectorAll('.base-button').forEach(btn => {
                   btn.classList.toggle('selected', btn.dataset.base === selectedPair.base);
               });
           } else {
               baseEditor.classList.add('opacity-25');
               selectedIndex.textContent = '-';
               selectedBase.textContent = '-';
               complementaryBase.textContent = '-';
               document.querySelectorAll('.base-button').forEach(btn => btn.classList.remove('selected'));
           }
       }

       function init3D() {
           scene = new THREE.Scene();
           camera = new THREE.PerspectiveCamera(75, 1000 / 1000, 0.1, 1000);
           camera.position.set(0, 0, 20);
           renderer = new THREE.WebGLRenderer({ antialias: true });
           renderer.setSize(1000, 1000);
           simContainer.appendChild(renderer.domElement);

           scene.add(new THREE.AmbientLight(0xaaaaaa, 0.8));
           const light = new THREE.PointLight(0xffffff, 1.2);
           light.position.set(10, 10, 10);
           scene.add(light);

           controls = new OrbitControls(camera, renderer.domElement);
           controls.enableDamping = true;
       }

       function buildDNA() {
           if (dnaGroup) scene.remove(dnaGroup);
           dnaGroup = new THREE.Group();
           interactiveObjects = [];
           selectedPair = null;
           updateEditorPanel();

           const radius = 2;
           const baseHeight = 0.6;
           const totalHeight = dnaSequence.length * baseHeight;
           const turns = dnaSequence.length / 10;

           const backboneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });

           for (let i = 0; i < dnaSequence.length; i++) {
               const y = -totalHeight / 2 + i * baseHeight;
               const angle = (i / dnaSequence.length) * turns * 2 * Math.PI;

               // sugar-phosphate backbone as spheres for click targets
               const b1 = new THREE.Mesh(new THREE.SphereGeometry(0.2, 16, 16), backboneMaterial);
               b1.position.set(radius * Math.cos(angle), y, radius * Math.sin(angle));
               const b2 = new THREE.Mesh(new THREE.SphereGeometry(0.2, 16, 16), backboneMaterial);
               b2.position.set(radius * Math.cos(angle + Math.PI), y, radius * Math.sin(angle + Math.PI));
               dnaGroup.add(b1, b2);

               const base1_char = dnaSequence[i];
               const base2_char = BASE_PAIRS[base1_char];

               const material1 = new THREE.MeshStandardMaterial({ color: BASE_COLORS[base1_char] });
               const material2 = new THREE.MeshStandardMaterial({ color: BASE_COLORS[base2_char] });

               const baseGeo = new THREE.CylinderGeometry(0.25, 0.25, 0.8, 16);

               const mesh1 = new THREE.Mesh(baseGeo, material1);
               mesh1.position.set((radius / 2) * Math.cos(angle), y, (radius / 2) * Math.sin(angle));
               mesh1.rotation.z = Math.PI / 2;

               const mesh2 = new THREE.Mesh(baseGeo, material2);
               mesh2.position.set((radius / 2) * Math.cos(angle + Math.PI), y, (radius / 2) * Math.sin(angle + Math.PI));
               mesh2.rotation.z = Math.PI / 2;

               dnaGroup.add(mesh1, mesh2);

               interactiveObjects.push({ base: base1_char, pair: base2_char, mesh1, mesh2, index: i });
           }
           scene.add(dnaGroup);
       }

       function onCanvasClick(event) {
           const raycaster = new THREE.Raycaster();
           const mouse = new THREE.Vector2();
           const rect = renderer.domElement.getBoundingClientRect();
           mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
           mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
           raycaster.setFromCamera(mouse, camera);
           const meshes = interactiveObjects.flatMap(obj => [obj.mesh1, obj.mesh2]);
           const intersects = raycaster.intersectObjects(meshes, true);

           if (intersects.length > 0) {
               const clickedMesh = intersects[0].object;
               for (const pair of interactiveObjects) {
                   if (pair.mesh1 === clickedMesh || pair.mesh2 === clickedMesh) {
                       selectedPair = pair;
                       break;
                   }
               }
           } else {
               selectedPair = null;
           }
           updateEditorPanel();
       }

       function handleBaseChange(newBase) {
           if (!selectedPair) return;
           const index = selectedPair.index;
           const newPair = BASE_PAIRS[newBase];
           dnaSequence = dnaSequence.substring(0, index) + newBase + dnaSequence.substring(index + 1);
           selectedPair.base = newBase;
           selectedPair.pair = newPair;
           selectedPair.mesh1.material.color.setHex(BASE_COLORS[newBase]);
           selectedPair.mesh2.material.color.setHex(BASE_COLORS[newPair]);
           updateEditorPanel();
       }

       function animate3D() {
           requestAnimationFrame(animate3D);
           if (controls) controls.update();
           if (dnaGroup) dnaGroup.rotation.y += 0.002;
           renderer.render(scene, camera);
       }

       initialize();
   </script>
</body>
</html>
 

We need your consent to load the translations

We use a third-party service to translate the website content that may collect data about your activity. Please review the details in the privacy policy and accept the service to view the translations.