Spaces:
Running
Running
Download bioai-platform/frontend/src/components/three/DNAHelix.tsx from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 9.6 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/three/DNAHelix.tsx
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/bioai-platform/frontend/src/components/three/DNAHelix.tsx
-
curl -L -o DNAHelix.tsx https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/bioai-platform/frontend/src/components/three/DNAHelix.tsx
9.6 kB
| 'use client'; | |
| import { useEffect, useRef } from 'react'; | |
| import * as THREE from 'three'; | |
| import { useTheme } from '@/contexts/theme'; | |
| /** | |
| * Binary DNA helix — the two backbone strands are threaded with 0/1 glyphs | |
| * (billboard sprites from a canvas texture) that tick like a living data | |
| * stream. Slow rotation, a gentle bob, pointer parallax and ambient particle | |
| * dust keep it alive without shouting. Reduced motion renders a static frame | |
| * (apple-design §14). Colors follow the Synteny signal palette in globals.css: | |
| * mint primary strand + cool blue secondary strand. | |
| */ | |
| export default function DNAHelix({ className }: { className?: string }) { | |
| const containerRef = useRef<HTMLDivElement>(null); | |
| const { theme } = useTheme(); | |
| useEffect(() => { | |
| const container = containerRef.current; | |
| if (!container) return; | |
| const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| const isLight = theme === 'light'; | |
| const digitA = isLight ? '#15803D' : '#4ADE80'; | |
| const digitB = isLight ? '#2563EB' : '#60A5FA'; | |
| const strandA = isLight ? '#15803D' : '#22C55E'; | |
| const strandB = isLight ? '#2563EB' : '#60A5FA'; | |
| const rung = isLight ? '#059669' : '#2FBF6E'; | |
| const particle = isLight ? '#15803D' : '#4ADE80'; | |
| const additive = !isLight; | |
| const W = container.clientWidth || 1; | |
| const H = container.clientHeight || 1; | |
| const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setSize(W, H); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.setClearColor(0x000000, 0); | |
| container.appendChild(renderer.domElement); | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(50, W / H, 0.1, 100); | |
| camera.position.set(0, 0, 9); | |
| const group = new THREE.Group(); | |
| scene.add(group); | |
| const TURNS = 2.2; | |
| const HEIGHT = 7.4; | |
| const RADIUS = 1.35; | |
| const N_RUNG = 15; | |
| const N_DIGITS = 44; | |
| const helix = (offset: number) => { | |
| const pts: THREE.Vector3[] = []; | |
| const steps = 160; | |
| for (let i = 0; i <= steps; i++) { | |
| const frac = i / steps; | |
| const theta = frac * TURNS * Math.PI * 2 + offset; | |
| const y = frac * HEIGHT - HEIGHT / 2; | |
| pts.push(new THREE.Vector3(Math.cos(theta) * RADIUS, y, Math.sin(theta) * RADIUS)); | |
| } | |
| return new THREE.CatmullRomCurve3(pts); | |
| }; | |
| const curveA = helix(0); | |
| const curveB = helix(Math.PI); | |
| const tubeMat = new THREE.MeshBasicMaterial({ | |
| color: strandA, | |
| transparent: true, | |
| opacity: isLight ? 0.28 : 0.16, | |
| depthWrite: false, | |
| }); | |
| group.add(new THREE.Mesh(new THREE.TubeGeometry(curveA, 120, 0.032, 4, false), tubeMat)); | |
| const tubeMatB = new THREE.MeshBasicMaterial({ | |
| color: strandB, | |
| transparent: true, | |
| opacity: isLight ? 0.22 : 0.12, | |
| depthWrite: false, | |
| }); | |
| group.add(new THREE.Mesh(new THREE.TubeGeometry(curveB, 120, 0.032, 4, false), tubeMatB)); | |
| const rungMat = new THREE.MeshBasicMaterial({ | |
| color: rung, | |
| transparent: true, | |
| opacity: isLight ? 0.22 : 0.11, | |
| depthWrite: false, | |
| }); | |
| const rungGeom = new THREE.CylinderGeometry(0.014, 0.014, 1, 4); | |
| rungGeom.translate(0, 0.5, 0); | |
| for (let i = 0; i < N_RUNG; i++) { | |
| const t = i / (N_RUNG - 1); | |
| const p1 = curveA.getPointAt(t); | |
| const p2 = curveB.getPointAt(t); | |
| const dir = p2.clone().sub(p1); | |
| const mesh = new THREE.Mesh(rungGeom, rungMat); | |
| mesh.position.copy(p1); | |
| mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.clone().normalize()); | |
| mesh.scale.set(1, dir.length(), 1); | |
| group.add(mesh); | |
| } | |
| const makeGlyph = (char: string, color: string) => { | |
| const size = 96; | |
| const cv = document.createElement('canvas'); | |
| cv.width = size; | |
| cv.height = size; | |
| const ctx = cv.getContext('2d'); | |
| if (ctx) { | |
| ctx.clearRect(0, 0, size, size); | |
| ctx.font = `600 ${Math.round(size * 0.72)}px "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillStyle = color; | |
| ctx.fillText(char, size / 2, size / 2 + size * 0.04); | |
| } | |
| return new THREE.CanvasTexture(cv); | |
| }; | |
| const mkSpriteMat = (tex: THREE.Texture, opacity: number) => | |
| new THREE.SpriteMaterial({ | |
| map: tex, | |
| transparent: true, | |
| depthWrite: false, | |
| opacity, | |
| blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending, | |
| }); | |
| const texA0 = makeGlyph('0', digitA); | |
| const texA1 = makeGlyph('1', digitA); | |
| const texB0 = makeGlyph('0', digitB); | |
| const texB1 = makeGlyph('1', digitB); | |
| const matsA = [mkSpriteMat(texA0, isLight ? 0.95 : 0.9), mkSpriteMat(texA1, isLight ? 0.95 : 0.9)]; | |
| const matsB = [mkSpriteMat(texB0, isLight ? 0.9 : 0.72), mkSpriteMat(texB1, isLight ? 0.9 : 0.72)]; | |
| type Glyph = { | |
| sprite: THREE.Sprite; | |
| mats: THREE.SpriteMaterial[]; | |
| base: number; | |
| curve: THREE.CatmullRomCurve3; | |
| rate: number; | |
| index: number; | |
| }; | |
| const glyphs: Glyph[] = []; | |
| const spawnGlyphs = (curve: THREE.CatmullRomCurve3, mats: THREE.SpriteMaterial[], rateBase: number) => { | |
| for (let i = 0; i < N_DIGITS; i++) { | |
| const sprite = new THREE.Sprite(mats[i % 2]); | |
| sprite.scale.setScalar(0.36); | |
| group.add(sprite); | |
| glyphs.push({ | |
| sprite, | |
| mats, | |
| base: i / N_DIGITS, | |
| curve, | |
| rate: rateBase + (i % 7) * 0.06, | |
| index: i, | |
| }); | |
| } | |
| }; | |
| spawnGlyphs(curveA, matsA, 1.4); | |
| spawnGlyphs(curveB, matsB, 0.9); | |
| const particleCount = 150; | |
| const particlePos = new Float32Array(particleCount * 3); | |
| const particleSpeed: number[] = []; | |
| for (let i = 0; i < particleCount; i++) { | |
| particlePos[i * 3] = (Math.random() - 0.5) * 14; | |
| particlePos[i * 3 + 1] = (Math.random() - 0.5) * 10; | |
| particlePos[i * 3 + 2] = (Math.random() - 0.5) * 8; | |
| particleSpeed.push(0.05 + Math.random() * 0.2); | |
| } | |
| const particleGeom = new THREE.BufferGeometry(); | |
| particleGeom.setAttribute('position', new THREE.BufferAttribute(particlePos, 3)); | |
| const particleMat = new THREE.PointsMaterial({ | |
| color: particle, | |
| size: 0.045, | |
| transparent: true, | |
| opacity: isLight ? 0.28 : 0.2, | |
| depthWrite: false, | |
| sizeAttenuation: true, | |
| blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending, | |
| }); | |
| const particles = new THREE.Points(particleGeom, particleMat); | |
| group.add(particles); | |
| let targetRotX = 0; | |
| let targetRotY = 0; | |
| const onPointerMove = (e: PointerEvent) => { | |
| const nx = (e.clientX / window.innerWidth) * 2 - 1; | |
| const ny = (e.clientY / window.innerHeight) * 2 - 1; | |
| targetRotY = nx * 0.24; | |
| targetRotX = ny * 0.14; | |
| }; | |
| window.addEventListener('pointermove', onPointerMove); | |
| let lastW = W; | |
| let lastH = H; | |
| const resize = () => { | |
| const w = container.clientWidth || 1; | |
| const h = container.clientHeight || 1; | |
| if (w === lastW && h === lastH) return; | |
| lastW = w; | |
| lastH = h; | |
| renderer.setSize(w, h); | |
| camera.aspect = w / h; | |
| camera.updateProjectionMatrix(); | |
| renderer.render(scene, camera); | |
| }; | |
| const ro = new ResizeObserver(resize); | |
| ro.observe(container); | |
| const placeGlyph = (g: Glyph, t: number, bit: number) => { | |
| g.sprite.position.copy(g.curve.getPointAt(t)); | |
| g.sprite.material = g.mats[bit % 2]; | |
| }; | |
| let raf = 0; | |
| let elapsed = 0; | |
| const clock = new THREE.Clock(); | |
| const render = () => { | |
| renderer.render(scene, camera); | |
| if (!reducedMotion) raf = requestAnimationFrame(tick); | |
| }; | |
| const tick = () => { | |
| const dt = Math.min(clock.getDelta(), 0.05); | |
| elapsed += dt; | |
| group.rotation.y = elapsed * 0.22 + targetRotY * 0.35; | |
| group.rotation.x = THREE.MathUtils.lerp(group.rotation.x, targetRotX, dt * 2.5); | |
| group.position.y = Math.sin(elapsed * 0.5) * 0.12; | |
| for (const g of glyphs) { | |
| const flow = (elapsed * g.rate * 0.035) % 1; | |
| const bit = (g.index + Math.floor(elapsed * g.rate)) % 2; | |
| placeGlyph(g, (g.base + flow) % 1, bit); | |
| } | |
| const posArr = particleGeom.attributes.position.array as Float32Array; | |
| for (let i = 0; i < particleCount; i++) { | |
| posArr[i * 3 + 1] += particleSpeed[i] * dt; | |
| if (posArr[i * 3 + 1] > 5.2) posArr[i * 3 + 1] = -5.2; | |
| } | |
| (particleGeom.attributes.position as THREE.BufferAttribute).needsUpdate = true; | |
| render(); | |
| }; | |
| if (reducedMotion) { | |
| for (const g of glyphs) placeGlyph(g, g.base, g.index % 2); | |
| group.rotation.y = 0.5; | |
| render(); | |
| } else { | |
| raf = requestAnimationFrame(tick); | |
| } | |
| return () => { | |
| cancelAnimationFrame(raf); | |
| window.removeEventListener('pointermove', onPointerMove); | |
| ro.disconnect(); | |
| renderer.dispose(); | |
| particleGeom.dispose(); | |
| particleMat.dispose(); | |
| tubeMat.dispose(); | |
| tubeMatB.dispose(); | |
| rungMat.dispose(); | |
| rungGeom.dispose(); | |
| [texA0, texA1, texB0, texB1].forEach((t) => t.dispose()); | |
| if (renderer.domElement.parentNode === container) { | |
| container.removeChild(renderer.domElement); | |
| } | |
| }; | |
| }, [theme]); | |
| return ( | |
| <div ref={containerRef} className={className} aria-hidden aria-label="Animated double helix made of binary digits" /> | |
| ); | |
| } | |