/** * DNA Helix Visualizer * Immersive multi-layered double helix that fills the screen */ import * as THREE from 'three'; import { BaseVisualizer } from './base-visualizer.js'; const vertexShader = ` uniform float uTime; uniform float uLow; uniform float uMid; uniform float uHigh; uniform float uMouseX; uniform float uMouseY; attribute float aStrand; // 0 or 1 for which strand attribute float aProgress; // 0-1 position along helix attribute float aIsRung; // 1 if this is a connecting rung particle attribute float aLayer; // Which helix layer (0, 1, 2 for nested helixes) varying float vAlpha; varying float vStrand; varying float vLayer; void main() { vec3 pos = position; // Layer-based radius - nested helixes float baseRadius = 8.0 + aLayer * 12.0; // Inner: 8, Mid: 20, Outer: 32 float helixRadius = baseRadius + uLow * 6.0; // Bass expands all layers float helixHeight = 100.0 * (1.0 - uLow * 0.3); // Compress with bass float twists = 5.0 - aLayer * 1.0; // Inner twists more // Rotation speed - layers rotate at different speeds float layerSpeed = 1.0 - aLayer * 0.2; float rotationSpeed = (0.6 + uMid * 2.0) * layerSpeed; float timeOffset = uTime * rotationSpeed; // Reverse rotation for alternate layers if (mod(aLayer, 2.0) > 0.5) { timeOffset = -timeOffset; } // Calculate helix position float t = aProgress; float angle = t * twists * 6.28318 + timeOffset; // Offset for second strand if (aStrand > 0.5 && aIsRung < 0.5) { angle += 3.14159; } // Base helix position - along Z-axis (viewer inside helix) float x = cos(angle) * helixRadius; float y = sin(angle) * helixRadius; float z = (t - 0.5) * helixHeight; // Rungs connect the two strands if (aIsRung > 0.5) { float rungAngle = t * twists * 6.28318 + timeOffset; float rungT = fract(aStrand * 10.0); float angle1 = rungAngle; float angle2 = rungAngle + 3.14159; x = mix(cos(angle1), cos(angle2), rungT) * helixRadius; y = mix(sin(angle1), sin(angle2), rungT) * helixRadius; // Rungs pulse dramatically with beat float pulse = 1.0 + uLow * 0.5; x *= pulse; y *= pulse; } // High frequencies add jitter/scatter - more on outer layers float jitterAmount = uHigh * (3.0 + aLayer * 2.0); x += sin(t * 40.0 + uTime * 8.0 + aLayer) * jitterAmount; y += cos(t * 40.0 + uTime * 8.0) * jitterAmount * 0.5; z += sin(t * 40.0 + uTime * 8.0 + 1.57 + aLayer) * jitterAmount; // Mouse offset - helix follows mouse position subtly x -= uMouseX * 1.0; y -= uMouseY * 0.7; // Bounce toward viewer with bass z += uLow * 10.0; pos = vec3(x, y, z); vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); // Point size - larger for outer layers float size = 3.0 + aLayer * 1.0; if (aIsRung > 0.5) { size = 2.0 + uLow * 1.5; } size *= (1.0 + uLow * 0.3); gl_PointSize = size * (300.0 / -mvPosition.z); // Alpha - outer layers slightly more transparent vAlpha = 0.8 - aLayer * 0.1 + uLow * 0.2; if (aIsRung > 0.5) { vAlpha *= 0.7; } vStrand = aStrand; vLayer = aLayer; gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` varying float vAlpha; varying float vStrand; varying float vLayer; void main() { float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard; float alpha = 1.0 - smoothstep(0.1, 0.5, dist); alpha *= vAlpha; // Pure white vec3 color = vec3(1.0); gl_FragColor = vec4(color, alpha); } `; export class HelixVisualizer extends BaseVisualizer { static name = 'Helix'; constructor(scene) { super(scene); this.particlesPerStrand = 400; this.rungsCount = 60; this.layers = 1; // Single helix this.init(); } init() { const rungsPerLayer = this.rungsCount; const particlesPerRung = 15; const totalParticles = this.layers * rungsPerLayer * particlesPerRung; const positions = new Float32Array(totalParticles * 3); const strands = new Float32Array(totalParticles); const progress = new Float32Array(totalParticles); const isRung = new Float32Array(totalParticles); const layers = new Float32Array(totalParticles); let idx = 0; for (let layer = 0; layer < this.layers; layer++) { // Rungs only (connecting particles between strands) for (let r = 0; r < rungsPerLayer; r++) { const rungProgress = r / rungsPerLayer; for (let p = 0; p < particlesPerRung; p++) { positions[idx * 3] = 0; positions[idx * 3 + 1] = 0; positions[idx * 3 + 2] = 0; strands[idx] = p / (particlesPerRung - 1); progress[idx] = rungProgress; isRung[idx] = 1; layers[idx] = layer; idx++; } } } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('aStrand', new THREE.BufferAttribute(strands, 1)); geometry.setAttribute('aProgress', new THREE.BufferAttribute(progress, 1)); geometry.setAttribute('aIsRung', new THREE.BufferAttribute(isRung, 1)); geometry.setAttribute('aLayer', new THREE.BufferAttribute(layers, 1)); this.material = this.createShaderMaterial(vertexShader, fragmentShader, { uMouseX: { value: 0 }, uMouseY: { value: 0 } }); this.mesh = new THREE.Points(geometry, this.material); } update(bands, time, mouse = { x: 0, y: 0 }) { if (!this.material) return; this.material.uniforms.uTime.value = time; this.material.uniforms.uLow.value = bands.low || 0; this.material.uniforms.uMid.value = bands.mid || 0; this.material.uniforms.uHigh.value = bands.high || 0; this.material.uniforms.uMouseX.value = mouse.x; this.material.uniforms.uMouseY.value = mouse.y; } } export default HelixVisualizer;