Liveweave: Generative AI Web Editor & HTML/CSS/JS Playground
Initializing
Liveweave
Web
expand_more
home
Home
Palette
Color Explorer
arrow_outward
Polyline
Graphics Editor
arrow_outward
frame_source
Python Playground
arrow_outward
build
Tools
expand_more
restart_alt
Load "Hello Weaver!"
post_add
Generate Lorem ipsum...
code
Format HTML
code_blocks
Format CSS
data_object
Format JavaScript
library_add
Library
expand_more
A
Algolia JS
Animate CSS
Apex Charts JS
B
Bulma CSS
Bootstrap
C
Chart JS
Chartist
Create JS
D
D3
Dojo
F
Foundation
Fullpage JS
G
Granim JS
Google Charts
H
Halfmoon
J
jQuery
M
Materialize
Moment JS
Masonry JS
Milligram CSS
P
Pure CSS
Primer CSS
Popper JS
Pattern CSS
Picnic CSS
R
React JS
Raphael JS
Raisin CSS
S
Semantic UI
Skeleton CSS
Spectre CSS
Tachyons CSS
T
Tailwind
Three JS
U
UI Kit
Vis JS
W
Water CSS
download
Download
expand_more
developer_mode
Download as HTML
folder_zip
Download as .ZIP
cloud_upload
Fork
account_circle
Login
settings
Settings
expand_more
Editor font
14
px
A
A
Night mode
Line number
Mini map
Word wrap
sync_alt
Reset Settings
smart_display
Run
left_panel_close
AI Assistant
dashboard
All
HTML
CSS
JS
visibility
Preview
bolt
Live Preview
terminal
JS Console
<div class="dna-app"> <header class="topbar"> <div class="brand"> <div class="logo"></div> <div> <h1>DNA Explorer</h1> <span>Interactive 3D Model</span> </div> </div> <div class="header-status"> <span></span> THREE.JS MOLECULAR MODEL </div> </header> <main class="workspace"> <!-- LEFT PANEL --> <aside class="panel left-panel"> <div class="panel-title"> Nucleobases </div> <div class="base-list"> <div class="base-item"> <span class="legend-sphere adenine"></span> <strong>A</strong> <div> Adenine <small>Purine</small> </div> </div> <div class="base-item"> <span class="legend-sphere thymine"></span> <strong>T</strong> <div> Thymine <small>Pyrimidine</small> </div> </div> <div class="base-item"> <span class="legend-sphere guanine"></span> <strong>G</strong> <div> Guanine <small>Purine</small> </div> </div> <div class="base-item"> <span class="legend-sphere cytosine"></span> <strong>C</strong> <div> Cytosine <small>Pyrimidine</small> </div> </div> </div> <div class="divider"></div> <div class="panel-title"> Controls </div> <div class="control-row"> <span>Rotation</span> <button id="playButton" class="icon-button" > Ⅱ </button> </div> <div class="control-row"> <span>Auto Rotate</span> <label class="switch"> <input id="autoRotate" type="checkbox" checked > <span class="switch-slider"></span> </label> </div> <div class="speed-control"> <div class="speed-heading"> <span>Rotation Speed</span> <strong id="speedLabel">1.0×</strong> </div> <input id="speedSlider" type="range" min="0" max="3" step=".1" value="1" > </div> <div class="subheading"> Display Options </div> <label class="check-row"> <input id="showLabels" type="checkbox" checked > <span>Show Base Labels</span> </label> <label class="check-row"> <input id="showBonds" type="checkbox" checked > <span>Show Hydrogen Bonds</span> </label> <label class="check-row"> <input id="showBackbone" type="checkbox" checked > <span>Show Backbone</span> </label> <button id="resetButton" class="reset-button" > ↻ Reset View </button> </aside> <!-- 3D VIEWER --> <section class="viewer"> <div id="threeContainer" class="three-container" ></div> <div class="viewer-label"> <span>3D MOLECULAR VISUALIZATION</span> <strong>DNA Double Helix</strong> </div> <div class="sequence-label"> <span>SEQUENCE</span> <strong> ATGCAAGCTGACGTAC </strong> </div> <div id="tooltip" class="tooltip" > Adenine (A) </div> <div class="viewer-help"> <span>◉</span> Click a base to inspect <i></i> Drag to rotate <i></i> Scroll to zoom </div> </section> <!-- RIGHT PANEL --> <aside class="right-column"> <section class="panel selected-panel"> <span class="eyebrow"> SELECTED BASE PAIR </span> <h2 id="pairTitle"> Adenine — Thymine </h2> <div class="selected-bases"> <div id="selectedLeft" class="big-base adenine" > A </div> <div id="selectedBonds" class="bond-preview" > <i></i> <i></i> </div> <div id="selectedRight" class="big-base thymine" > T </div> </div> <div class="properties"> <div> <span>Pair Type</span> <strong id="pairType"> A – T </strong> </div> <div> <span>Hydrogen Bonds</span> <strong id="bondCount"> 2 </strong> </div> <div> <span>Classification</span> <strong> Watson–Crick </strong> </div> </div> <p id="description"> Adenine (A) pairs with thymine (T) using two hydrogen bonds, helping to stabilize the DNA double helix. </p> </section> <section class="panel about-panel"> <div class="panel-title"> About DNA </div> <p> DNA (deoxyribonucleic acid) carries genetic information in living organisms. It consists of two antiparallel strands forming a double helix. </p> <p> Complementary base pairs (A–T and G–C) are held together by hydrogen bonds. </p> </section> </aside> </main> </div> <!-- Three.js --> <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js" ></script>
* { box-sizing: border-box; } :root { --background: #020b13; --panel: rgba(7, 22, 34, .92); --border: rgba(103, 180, 225, .15); --text: #e5f1fb; --muted: #7895ae; --blue: #188cff; --red: #ff4b52; --yellow: #ffc53d; --green: #57d77d; } html, body { margin: 0; min-height: 100%; } body { background: radial-gradient( circle at 50% 20%, #092239, #020b13 55% ); color: var(--text); font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } button, input { font: inherit; } /* =============================== APP ================================ */ .dna-app { min-height: 100vh; } /* =============================== HEADER ================================ */ .topbar { height: 76px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; border-bottom: 1px solid var(--border); background: rgba(2,11,19,.82); backdrop-filter: blur(18px); } .brand { display: flex; align-items: center; gap: 12px; } .logo { font-size: 33px; } .brand h1 { margin: 0; font-size: 20px; font-weight: 600; } .brand span { display: block; margin-top: 3px; color: #789abd; font-size: 10px; } .header-status { display: flex; align-items: center; gap: 7px; color: #64819c; font-size: 7px; letter-spacing: 1px; } .header-status > span { width: 7px; height: 7px; border-radius: 50%; background: #4ce095; box-shadow: 0 0 12px rgba(76,224,149,.7); } /* =============================== WORKSPACE ================================ */ .workspace { width: 100%; min-height: calc(100vh - 76px); display: grid; grid-template-columns: 245px minmax(420px, 1fr) 285px; gap: 16px; padding: 16px; } /* =============================== PANELS ================================ */ .panel { border: 1px solid var(--border); border-radius: 14px; background: linear-gradient( 145deg, rgba(9,27,42,.96), rgba(4,17,28,.96) ); box-shadow: 0 20px 60px rgba(0,0,0,.18); } .left-panel { align-self: start; padding: 18px; } .panel-title { color: #e3eff9; font-size: 12px; font-weight: 600; } /* =============================== BASE LIST ================================ */ .base-list { margin-top: 17px; display: flex; flex-direction: column; gap: 14px; } .base-item { display: grid; grid-template-columns: 34px 25px 1fr; align-items: center; gap: 8px; color: #d9e7f2; font-size: 9px; } .base-item > strong { font-size: 15px; color: #bcd5e8; } .base-item small { display: block; margin-top: 2px; color: #607e99; font-size: 7px; } /* =============================== CSS LEGEND SPHERES ================================ */ .legend-sphere { width: 31px; height: 31px; display: block; border-radius: 50%; box-shadow: inset -6px -7px 8px rgba(0,0,0,.4), inset 5px 5px 7px rgba(255,255,255,.35), 0 4px 10px rgba(0,0,0,.4); } .adenine { background: radial-gradient( circle at 30% 25%, #b9ecff, #168fff 30%, #06468c 72%, #022451 ); } .thymine { background: radial-gradient( circle at 30% 25%, #ffd1d3, #ff5157 30%, #a51e2d 72%, #5c0a17 ); } .guanine { background: radial-gradient( circle at 30% 25%, #fff2ae, #ffc43b 30%, #a46c05 72%, #553300 ); } .cytosine { background: radial-gradient( circle at 30% 25%, #d0ffdc, #57d87c 30%, #19733b 72%, #073c20 ); } /* =============================== DIVIDER ================================ */ .divider { height: 1px; margin: 19px 0; background: var(--border); } /* =============================== CONTROLS ================================ */ .control-row { display: flex; align-items: center; justify-content: space-between; margin-top: 13px; color: #82a0bb; font-size: 8px; } .icon-button { width: 31px; height: 28px; border: 0; border-radius: 7px; background: #0b67b9; color: white; cursor: pointer; } /* switch */ .switch { position: relative; width: 34px; height: 19px; } .switch input { opacity: 0; } .switch-slider { position: absolute; inset: 0; border-radius: 20px; background: #263b4c; cursor: pointer; transition: .2s; } .switch-slider::after { content: ""; position: absolute; width: 15px; height: 15px; left: 2px; top: 2px; border-radius: 50%; background: white; transition: .2s; } .switch input:checked + .switch-slider { background: #168cff; } .switch input:checked + .switch-slider::after { transform: translateX(15px); } /* speed */ .speed-control { margin-top: 15px; } .speed-heading { display: flex; justify-content: space-between; margin-bottom: 8px; color: #82a0bb; font-size: 8px; } .speed-heading strong { color: #c9deef; font-weight: 500; } .speed-control input { width: 100%; accent-color: #168cff; } .subheading { margin: 17px 0 10px; color: #d5e6f4; font-size: 10px; } /* checkboxes */ .check-row { display: flex; align-items: center; gap: 7px; margin-top: 8px; color: #8ba7c0; font-size: 8px; } .check-row input { accent-color: #168cff; } .reset-button { margin-top: 19px; padding: 9px 12px; border: 1px solid rgba(46,143,210,.25); border-radius: 8px; background: #0d3858; color: #d4e8f8; font-size: 8px; cursor: pointer; } .reset-button:hover { background: #114c77; } /* =============================== THREE.JS VIEWER ================================ */ .viewer { position: relative; min-height: 720px; overflow: hidden; border: 1px solid var(--border); border-radius: 16px; background: radial-gradient( circle at 50% 45%, #08273e, #020b13 68% ); } .three-container { position: absolute; inset: 0; } .three-container canvas { display: block; width: 100%; height: 100%; } /* viewer text */ .viewer-label { position: absolute; left: 17px; top: 15px; pointer-events: none; } .viewer-label span { display: block; color: #517794; font-size: 6px; letter-spacing: 1.3px; } .viewer-label strong { display: block; margin-top: 3px; font-size: 11px; } .sequence-label { position: absolute; right: 17px; top: 15px; text-align: right; pointer-events: none; } .sequence-label span { display: block; color: #517794; font-size: 6px; letter-spacing: 1px; } .sequence-label strong { display: block; margin-top: 3px; color: #7ea8c8; font-family: monospace; font-size: 8px; letter-spacing: 1px; } /* help */ .viewer-help { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 8px 12px; border: 1px solid var(--border); border-radius: 20px; background: rgba(2,11,19,.78); color: #718ca4; font-size: 7px; backdrop-filter: blur(10px); pointer-events: none; } .viewer-help i { width: 1px; height: 10px; background: rgba(255,255,255,.13); } /* tooltip */ .tooltip { position: absolute; display: none; z-index: 20; padding: 7px 9px; border: 1px solid var(--border); border-radius: 6px; background: rgba(0,8,15,.92); color: white; font-size: 8px; pointer-events: none; box-shadow: 0 6px 20px rgba(0,0,0,.35); } /* =============================== RIGHT COLUMN ================================ */ .right-column { display: flex; flex-direction: column; gap: 14px; } .selected-panel, .about-panel { padding: 18px; } .eyebrow { color: #66849d; font-size: 6px; font-weight: 700; letter-spacing: 1.2px; } .selected-panel h2 { margin: 5px 0 0; font-size: 16px; font-weight: 500; } /* =============================== SELECTED BASES ================================ */ .selected-bases { display: flex; align-items: center; justify-content: center; margin: 25px 0; } .big-base { position: relative; z-index: 2; width: 57px; height: 57px; display: grid; place-items: center; border-radius: 50%; color: white; font-size: 22px; font-weight: 500; text-shadow: 0 2px 5px rgba(0,0,0,.4); box-shadow: inset -8px -9px 11px rgba(0,0,0,.38), inset 7px 7px 10px rgba(255,255,255,.25), 0 5px 15px rgba(0,0,0,.4); } .bond-preview { width: 65px; display: flex; flex-direction: column; gap: 8px; } .bond-preview i { width: 100%; border-top: 2px dotted rgba(225,243,255,.8); } /* =============================== PROPERTIES ================================ */ .properties { display: flex; flex-direction: column; gap: 12px; margin-top: 5px; } .properties > div { display: flex; justify-content: space-between; gap: 10px; } .properties span { color: #7895ad; font-size: 8px; } .properties strong { color: #d9e8f3; font-size: 8px; font-weight: 500; } .selected-panel p, .about-panel p { color: #819bb1; font-size: 8px; line-height: 1.65; } .selected-panel p { margin: 18px 0 0; padding-top: 15px; border-top: 1px solid var(--border); } .about-panel p { margin: 10px 0 0; } /* =============================== RESPONSIVE ================================ */ @media (max-width: 1050px) { .workspace { grid-template-columns: 210px 1fr; } .right-column { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; } } @media (max-width: 760px) { .workspace { grid-template-columns: 1fr; } .left-panel { order: 2; } .viewer { order: 1; min-height: 600px; } .right-column { order: 3; grid-column: auto; grid-template-columns: 1fr; } } @media (max-width: 500px) { .topbar { padding: 0 14px; } .header-status { display: none; } .workspace { padding: 9px; } .viewer { min-height: 530px; } .sequence-label { display: none; } .viewer-help { font-size: 6px; } }
/* ===================================================== THREE.JS DNA EXPLORER ===================================================== */ /* ===================================================== BASIC THREE.JS SETUP ===================================================== */ const container = document.getElementById( "threeContainer" ); const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2( 0x020b13, 0.035 ); const camera = new THREE.PerspectiveCamera( 38, container.clientWidth / container.clientHeight, 0.1, 100 ); camera.position.set( 0, 0, 19 ); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio( Math.min( window.devicePixelRatio, 2 ) ); renderer.setSize( container.clientWidth, container.clientHeight ); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.25; container.appendChild( renderer.domElement ); /* ===================================================== LIGHTS ===================================================== */ /* Soft overall illumination */ const ambientLight = new THREE.AmbientLight( 0x7fa9d0, 1.25 ); scene.add( ambientLight ); /* Main white light */ const keyLight = new THREE.DirectionalLight( 0xffffff, 3.5 ); keyLight.position.set( 5, 8, 10 ); scene.add( keyLight ); /* Blue rim light */ const blueLight = new THREE.PointLight( 0x168cff, 25, 25 ); blueLight.position.set( -7, 3, 7 ); scene.add( blueLight ); /* Cool cyan light */ const cyanLight = new THREE.PointLight( 0x4bdcff, 18, 20 ); cyanLight.position.set( 6, -4, 5 ); scene.add( cyanLight ); /* Small warm light adds more realistic reflections. */ const warmLight = new THREE.PointLight( 0xff9d55, 10, 18 ); warmLight.position.set( 4, 1, -5 ); scene.add( warmLight ); /* ===================================================== DNA DATA ===================================================== */ const sequence = [ ["A", "T"], ["G", "C"], ["T", "A"], ["C", "G"], ["A", "T"], ["A", "T"], ["G", "C"], ["C", "G"], ["T", "A"], ["G", "C"], ["A", "T"], ["C", "G"], ["G", "C"], ["T", "A"], ["A", "T"], ["C", "G"], ["T", "A"], ["G", "C"] ]; const baseData = { A: { name: "Adenine", color: 0x168cff }, T: { name: "Thymine", color: 0xff4b52 }, G: { name: "Guanine", color: 0xffc43d }, C: { name: "Cytosine", color: 0x57d77d } }; /* ===================================================== DNA GROUP ===================================================== */ const dna = new THREE.Group(); scene.add( dna ); dna.rotation.x = -0.08; /* ===================================================== GEOMETRY ===================================================== */ const baseSphereGeometry = new THREE.SphereGeometry( 0.43, 40, 40 ); const backboneSphereGeometry = new THREE.SphereGeometry( 0.34, 36, 36 ); const tinySphereGeometry = new THREE.SphereGeometry( 0.15, 24, 24 ); /* ===================================================== MATERIAL CREATOR ===================================================== */ function createBaseMaterial( color ) { return new THREE.MeshPhysicalMaterial({ color: color, roughness: 0.18, metalness: 0.05, clearcoat: 1, clearcoatRoughness: 0.08, reflectivity: 1 }); } const materials = { A: createBaseMaterial( baseData.A.color ), T: createBaseMaterial( baseData.T.color ), G: createBaseMaterial( baseData.G.color ), C: createBaseMaterial( baseData.C.color ) }; const backboneMaterial = new THREE.MeshPhysicalMaterial({ color: 0x75bde8, roughness: .2, metalness: .15, clearcoat: 1, clearcoatRoughness: .1 }); const connectorMaterial = new THREE.MeshStandardMaterial({ color: 0xa5c6d9, roughness: .3, metalness: .35 }); const hydrogenMaterial = new THREE.MeshStandardMaterial({ color: 0xe2f4ff, emissive: 0x5ca5cc, emissiveIntensity: .4 }); /* ===================================================== CYLINDER BETWEEN TWO POINTS ===================================================== */ function createCylinderBetween( start, end, radius, material ) { const direction = new THREE.Vector3() .subVectors( end, start ); const length = direction.length(); const geometry = new THREE.CylinderGeometry( radius, radius, length, 16 ); const cylinder = new THREE.Mesh( geometry, material ); const midpoint = new THREE.Vector3() .addVectors( start, end ) .multiplyScalar(.5); cylinder.position.copy( midpoint ); cylinder.quaternion .setFromUnitVectors( new THREE.Vector3( 0, 1, 0 ), direction .clone() .normalize() ); return cylinder; } /* ===================================================== HYDROGEN BOND ===================================================== */ function createDottedBond( start, end, dots ) { const group = new THREE.Group(); for ( let i = 0; i < dots; i++ ) { const t = (i + 1) / (dots + 1); const position = new THREE.Vector3() .lerpVectors( start, end, t ); const dot = new THREE.Mesh( tinySphereGeometry, hydrogenMaterial ); dot.scale.setScalar( .38 ); dot.position.copy( position ); group.add( dot ); } group.userData.isHydrogenBond = true; return group; } /* ===================================================== LABEL SPRITES ===================================================== */ const labelSprites = []; function createTextSprite( text, color ) { const canvas = document.createElement( "canvas" ); canvas.width = 128; canvas.height = 128; const ctx = canvas.getContext( "2d" ); ctx.clearRect( 0, 0, 128, 128 ); ctx.font = "bold 54px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = "#ffffff"; ctx.shadowColor = "rgba(0,0,0,.65)"; ctx.shadowBlur = 4; ctx.fillText( text, 64, 66 ); const texture = new THREE.CanvasTexture( canvas ); texture.colorSpace = THREE.SRGBColorSpace; const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: true }); const sprite = new THREE.Sprite( material ); sprite.scale.set( .7, .7, .7 ); labelSprites.push( sprite ); return sprite; } /* ===================================================== BUILD DNA ===================================================== */ const baseMeshes = []; const backboneObjects = []; const hydrogenObjects = []; const pairGroups = []; const radius = 3.05; const verticalSpacing = .72; const angleStep = Math.PI / 5.25; const totalHeight = ( sequence.length - 1 ) * verticalSpacing; for ( let i = 0; i < sequence.length; i++ ) { const pair = sequence[i]; const leftType = pair[0]; const rightType = pair[1]; const angle = i * angleStep; const y = totalHeight / 2 - i * verticalSpacing; /* Opposite points around the helix. */ const left = new THREE.Vector3( Math.cos(angle) * radius, y, Math.sin(angle) * radius ); const right = new THREE.Vector3( Math.cos( angle + Math.PI ) * radius, y, Math.sin( angle + Math.PI ) * radius ); /* BASE PAIR GROUP */ const pairGroup = new THREE.Group(); pairGroup.userData = { pairIndex: i }; dna.add( pairGroup ); pairGroups.push( pairGroup ); /* Backbone spheres */ const leftBackbone = new THREE.Mesh( backboneSphereGeometry, backboneMaterial ); leftBackbone.position.copy( left ); pairGroup.add( leftBackbone ); const rightBackbone = new THREE.Mesh( backboneSphereGeometry, backboneMaterial ); rightBackbone.position.copy( right ); pairGroup.add( rightBackbone ); backboneObjects.push( leftBackbone, rightBackbone ); /* Move bases toward center */ const leftBasePosition = left.clone() .multiplyScalar(.62); leftBasePosition.y = y; const rightBasePosition = right.clone() .multiplyScalar(.62); rightBasePosition.y = y; /* Left base */ const leftBase = new THREE.Mesh( baseSphereGeometry, materials[leftType] ); leftBase.position.copy( leftBasePosition ); leftBase.userData = { isBase: true, pairIndex: i, type: leftType }; pairGroup.add( leftBase ); baseMeshes.push( leftBase ); /* Right base */ const rightBase = new THREE.Mesh( baseSphereGeometry, materials[rightType] ); rightBase.position.copy( rightBasePosition ); rightBase.userData = { isBase: true, pairIndex: i, type: rightType }; pairGroup.add( rightBase ); baseMeshes.push( rightBase ); /* Base labels */ const leftLabel = createTextSprite( leftType ); leftLabel.position.copy( leftBasePosition ); leftLabel.position.z += .44; pairGroup.add( leftLabel ); const rightLabel = createTextSprite( rightType ); rightLabel.position.copy( rightBasePosition ); rightLabel.position.z += .44; pairGroup.add( rightLabel ); /* Connector: backbone -> base */ const leftConnector = createCylinderBetween( left, leftBasePosition, .075, connectorMaterial ); pairGroup.add( leftConnector ); const rightConnector = createCylinderBetween( right, rightBasePosition, .075, connectorMaterial ); pairGroup.add( rightConnector ); backboneObjects.push( leftConnector, rightConnector ); /* Base pair connector */ const centerDirection = new THREE.Vector3() .subVectors( rightBasePosition, leftBasePosition ) .normalize(); const bondStart = leftBasePosition .clone() .add( centerDirection .clone() .multiplyScalar(.43) ); const bondEnd = rightBasePosition .clone() .add( centerDirection .clone() .multiplyScalar(-.43) ); /* A-T = 2 hydrogen bonds G-C = 3 hydrogen bonds */ const bondNumber = ( leftType === "G" || leftType === "C" ) ? 3 : 2; /* Offset multiple dotted bonds vertically. */ for ( let b = 0; b < bondNumber; b++ ) { const offset = ( b - ( bondNumber - 1 ) / 2 ) * .11; const start = bondStart.clone(); const end = bondEnd.clone(); start.y += offset; end.y += offset; const hydrogen = createDottedBond( start, end, 12 ); pairGroup.add( hydrogen ); hydrogenObjects.push( hydrogen ); } } /* ===================================================== BACKBONE CONNECTIONS ===================================================== */ for ( let i = 0; i < sequence.length - 1; i++ ) { const angle1 = i * angleStep; const angle2 = ( i + 1 ) * angleStep; const y1 = totalHeight / 2 - i * verticalSpacing; const y2 = totalHeight / 2 - ( i + 1 ) * verticalSpacing; const left1 = new THREE.Vector3( Math.cos(angle1) * radius, y1, Math.sin(angle1) * radius ); const left2 = new THREE.Vector3( Math.cos(angle2) * radius, y2, Math.sin(angle2) * radius ); const right1 = new THREE.Vector3( Math.cos( angle1 + Math.PI ) * radius, y1, Math.sin( angle1 + Math.PI ) * radius ); const right2 = new THREE.Vector3( Math.cos( angle2 + Math.PI ) * radius, y2, Math.sin( angle2 + Math.PI ) * radius ); const leftLink = createCylinderBetween( left1, left2, .11, backboneMaterial ); dna.add( leftLink ); const rightLink = createCylinderBetween( right1, right2, .11, backboneMaterial ); dna.add( rightLink ); backboneObjects.push( leftLink, rightLink ); } /* ===================================================== BACKGROUND PARTICLES ===================================================== */ const starGeometry = new THREE.BufferGeometry(); const starCount = 350; const starPositions = new Float32Array( starCount * 3 ); for ( let i = 0; i < starCount; i++ ) { starPositions[ i * 3 ] = ( Math.random() - .5 ) * 35; starPositions[ i * 3 + 1 ] = ( Math.random() - .5 ) * 30; starPositions[ i * 3 + 2 ] = ( Math.random() - .5 ) * 25; } starGeometry.setAttribute( "position", new THREE.BufferAttribute( starPositions, 3 ) ); const starMaterial = new THREE.PointsMaterial({ color: 0x4faee8, size: .035, transparent: true, opacity: .55 }); const stars = new THREE.Points( starGeometry, starMaterial ); scene.add( stars ); /* ===================================================== SELECTION ===================================================== */ let selectedPair = 0; const pairTitle = document.getElementById( "pairTitle" ); const pairType = document.getElementById( "pairType" ); const bondCount = document.getElementById( "bondCount" ); const description = document.getElementById( "description" ); const selectedLeft = document.getElementById( "selectedLeft" ); const selectedRight = document.getElementById( "selectedRight" ); const selectedBonds = document.getElementById( "selectedBonds" ); function selectPair( index ) { selectedPair = index; const pair = sequence[index]; const left = pair[0]; const right = pair[1]; /* Reset all base sizes */ baseMeshes.forEach( function(mesh) { mesh.scale.setScalar( 1 ); } ); /* Highlight selected pair */ baseMeshes.forEach( function(mesh) { if ( mesh.userData .pairIndex === index ) { mesh.scale.setScalar( 1.22 ); } } ); const leftName = baseData[left].name; const rightName = baseData[right].name; pairTitle.textContent = leftName + " — " + rightName; pairType.textContent = left + " – " + right; const bonds = ( left === "G" || left === "C" ) ? 3 : 2; bondCount.textContent = bonds; /* CSS preview spheres */ selectedLeft.className = "big-base " + getCSSClass(left); selectedLeft.textContent = left; selectedRight.className = "big-base " + getCSSClass(right); selectedRight.textContent = right; /* Preview dotted bonds */ selectedBonds.innerHTML = ""; for ( let i = 0; i < bonds; i++ ) { const bond = document.createElement( "i" ); selectedBonds.appendChild( bond ); } if ( bonds === 2 ) { description.textContent = "Adenine (A) pairs with thymine " + "(T) using two hydrogen bonds, " + "helping to stabilize the DNA " + "double helix."; } else { description.textContent = "Guanine (G) pairs with cytosine " + "(C) using three hydrogen bonds. " + "The additional hydrogen bond " + "makes G–C pairing more strongly " + "bonded than A–T."; } } function getCSSClass( base ) { if (base === "A") { return "adenine"; } if (base === "T") { return "thymine"; } if (base === "G") { return "guanine"; } return "cytosine"; } selectPair( 0 ); /* ===================================================== RAYCASTING ===================================================== */ const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const tooltip = document.getElementById( "tooltip" ); function getMousePosition( event ) { 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; } /* hover */ renderer.domElement .addEventListener( "pointermove", function(event) { if (dragging) { tooltip.style.display = "none"; return; } getMousePosition( event ); raycaster.setFromCamera( mouse, camera ); const hits = raycaster.intersectObjects( baseMeshes, false ); if ( hits.length ) { const mesh = hits[0].object; const type = mesh.userData.type; tooltip.textContent = baseData[type].name + " (" + type + ")"; const viewerRect = container .parentElement .getBoundingClientRect(); tooltip.style.left = ( event.clientX - viewerRect.left + 12 ) + "px"; tooltip.style.top = ( event.clientY - viewerRect.top + 12 ) + "px"; tooltip.style.display = "block"; renderer.domElement .style.cursor = "pointer"; } else { tooltip.style.display = "none"; renderer.domElement .style.cursor = "grab"; } } ); /* click */ renderer.domElement .addEventListener( "click", function(event) { if ( movedDuringDrag ) { return; } getMousePosition( event ); raycaster.setFromCamera( mouse, camera ); const hits = raycaster.intersectObjects( baseMeshes, false ); if ( hits.length ) { selectPair( hits[0] .object .userData .pairIndex ); } } ); /* ===================================================== DRAG ROTATION ===================================================== */ let dragging = false; let movedDuringDrag = false; let previousX = 0; let previousY = 0; renderer.domElement .addEventListener( "pointerdown", function(event) { dragging = true; movedDuringDrag = false; previousX = event.clientX; previousY = event.clientY; renderer.domElement .setPointerCapture( event.pointerId ); renderer.domElement .style.cursor = "grabbing"; } ); renderer.domElement .addEventListener( "pointermove", function(event) { if ( !dragging ) { return; } const dx = event.clientX - previousX; const dy = event.clientY - previousY; if ( Math.abs(dx) > 2 || Math.abs(dy) > 2 ) { movedDuringDrag = true; } dna.rotation.y += dx * .008; dna.rotation.x += dy * .005; dna.rotation.x = Math.max( -.75, Math.min( .75, dna.rotation.x ) ); previousX = event.clientX; previousY = event.clientY; } ); function endDrag() { dragging = false; renderer.domElement .style.cursor = "grab"; setTimeout( function() { movedDuringDrag = false; }, 20 ); } renderer.domElement .addEventListener( "pointerup", endDrag ); renderer.domElement .addEventListener( "pointercancel", endDrag ); /* ===================================================== ZOOM ===================================================== */ renderer.domElement .addEventListener( "wheel", function(event) { event.preventDefault(); camera.position.z += event.deltaY * .008; camera.position.z = Math.max( 11, Math.min( 28, camera.position.z ) ); }, { passive: false } ); /* ===================================================== CONTROLS ===================================================== */ let rotating = true; let rotationSpeed = 1; const playButton = document.getElementById( "playButton" ); const autoRotate = document.getElementById( "autoRotate" ); const speedSlider = document.getElementById( "speedSlider" ); const speedLabel = document.getElementById( "speedLabel" ); playButton.addEventListener( "click", function() { rotating = !rotating; autoRotate.checked = rotating; playButton.textContent = rotating ? "Ⅱ" : "▶"; } ); autoRotate.addEventListener( "change", function() { rotating = autoRotate.checked; playButton.textContent = rotating ? "Ⅱ" : "▶"; } ); speedSlider.addEventListener( "input", function() { rotationSpeed = Number( speedSlider.value ); speedLabel.textContent = rotationSpeed .toFixed(1) + "×"; } ); /* ===================================================== LABEL TOGGLE ===================================================== */ const showLabels = document.getElementById( "showLabels" ); showLabels.addEventListener( "change", function() { labelSprites.forEach( function(sprite) { sprite.visible = showLabels.checked; } ); } ); /* ===================================================== HYDROGEN BOND TOGGLE ===================================================== */ const showBonds = document.getElementById( "showBonds" ); showBonds.addEventListener( "change", function() { hydrogenObjects.forEach( function(object) { object.visible = showBonds.checked; } ); } ); /* ===================================================== BACKBONE TOGGLE ===================================================== */ const showBackbone = document.getElementById( "showBackbone" ); showBackbone.addEventListener( "change", function() { backboneObjects.forEach( function(object) { object.visible = showBackbone.checked; } ); } ); /* ===================================================== RESET ===================================================== */ document .getElementById( "resetButton" ) .addEventListener( "click", function() { dna.rotation.set( -.08, 0, 0 ); camera.position.set( 0, 0, 19 ); rotating = true; autoRotate.checked = true; playButton.textContent = "Ⅱ"; rotationSpeed = 1; speedSlider.value = 1; speedLabel.textContent = "1.0×"; selectPair( 0 ); } ); /* ===================================================== RESIZE ===================================================== */ function resize() { const width = container.clientWidth; const height = container.clientHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize( width, height ); } window.addEventListener( "resize", resize ); /* ===================================================== ANIMATION ===================================================== */ const clock = new THREE.Clock(); function animate() { requestAnimationFrame( animate ); const delta = clock.getDelta(); if ( rotating && !dragging ) { dna.rotation.y += delta * .32 * rotationSpeed; } /* Very subtle background motion. */ stars.rotation.y += delta * .004; /* Selected bases gently pulse. */ const pulse = 1.20 + Math.sin( performance.now() * .004 ) * .035; baseMeshes.forEach( function(mesh) { if ( mesh.userData .pairIndex === selectedPair ) { mesh.scale.setScalar( pulse ); } } ); renderer.render( scene, camera ); } animate();
terminal
JavaScript Console
Preview
Clear
close
›
Run