PageSourceSearch

https://ruvos.com/js/dna-helix.js?v=20260915

js ruvos.com collected 2026-10-03 19:41:21 UTC 8,363 bytes, 227 lines download raw bytes

1// DnaHelix — vanilla Three.js port of an R3F particle helix.
2// Renders into a canvas; mouse interaction repels particles which spring back.
3
4function DnaHelix({
5  color = '#dd3b3c',
6  className = '',
7  style = {}
8}) {
9  const containerRef = React.useRef(null);
10  React.useEffect(() => {
11    const container = containerRef.current;
12    if (!container || !window.THREE) return;
13    const THREE = window.THREE;
14
15    // ——— scene setup ———
16    const scene = new THREE.Scene();
17    const renderer = new THREE.WebGLRenderer({
18      alpha: true,
19      antialias: true
20    });
21    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
22    container.appendChild(renderer.domElement);
23    const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 2000);
24    camera.position.set(0, -28, -90);
25    camera.lookAt(0, 0, 0);
26    function resize() {
27      const w = container.clientWidth;
28      const h = container.clientHeight;
29      renderer.setSize(w, h);
30      camera.aspect = w / h;
31      camera.updateProjectionMatrix();
32    }
33    resize();
34    const ro = new ResizeObserver(resize);
35    ro.observe(container);
36
37    // ——— geometry: helix + connecting rungs ———
38    const numHelix = 6000;
39    const numLineSpace = 60;
40    const numLine = 100;
41    const numAmount = numHelix + numLineSpace * numLine;
42    const positions = new Float32Array(numAmount * 3);
43    const radians = new Float32Array(numAmount);
44    const radiuses = new Float32Array(numAmount);
45    const delays = new Float32Array(numAmount);
46    const velocities = new Float32Array(numAmount * 3);
47    for (let i = 0; i < numHelix; i++) {
48      const r = Math.random();
49      const dx = (Math.random() * 2 - 1) * r * 6;
50      const dy = (Math.random() * 2 - 1) * r * 6;
51      const dz = (Math.random() * 2 - 1) * r * 6;
52      positions[i * 3 + 0] = (i / numHelix * 2 - 1) * 150 + dx;
53      positions[i * 3 + 1] = dy;
54      positions[i * 3 + 2] = dz;
55      radians[i] = THREE.MathUtils.degToRad(i / numHelix * 900 + i % 2 * 180);
56      radiuses[i] = 18;
57      delays[i] = THREE.MathUtils.degToRad(Math.random() * 360);
58    }
59    for (let j = 0; j < numLineSpace; j++) {
60      const radValue = THREE.MathUtils.degToRad(j / numLineSpace * 900);
61      for (let k = 0; k < numLine; k++) {
62        const idx = j * numLine + k + numHelix;
63        const r = Math.random();
64        const dx = (Math.random() * 2 - 1) * r;
65        const dy = (Math.random() * 2 - 1) * r;
66        const dz = (Math.random() * 2 - 1) * r;
67        positions[idx * 3 + 0] = (j / numLineSpace * 2 - 1) * 150 + dx;
68        positions[idx * 3 + 1] = dy;
69        positions[idx * 3 + 2] = dz;
70        radians[idx] = radValue;
71        radiuses[idx] = (k / numLine * 2 - 1) * 18;
72        delays[idx] = THREE.MathUtils.degToRad(Math.random() * 360);
73      }
74    }
75    const initialPositions = positions.slice();
76    const geometry = new THREE.BufferGeometry();
77    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
78    geometry.setAttribute('radian', new THREE.BufferAttribute(radians, 1));
79    geometry.setAttribute('radius', new THREE.BufferAttribute(radiuses, 1));
80    geometry.setAttribute('delay', new THREE.BufferAttribute(delays, 1));
81    const uniforms = {
82      time: {
83        value: 0
84      },
85      colorA: {
86        value: new THREE.Color('#EE3E3E')
87      },
88      colorB: {
89        value: new THREE.Color('#3772FF')
90      },
91      mouse: {
92        value: new THREE.Vector3(0, 0, 0)
93      }
94    };
95    const material = new THREE.ShaderMaterial({
96      uniforms,
97      transparent: true,
98      depthWrite: false,
99      blending: THREE.NormalBlending,
100      vertexShader: `
101        attribute float radian;
102        attribute float radius;
103        attribute float delay;
104        uniform float time;
105        varying float vMix;
106        void main() {
107          vec3 updatePosition = position
108            + vec3(
109              sin(-time * 4.0 + delay),
110              sin(radian + -time * 0.4) * (radius + sin(-time * 4.0 + delay)),
111              cos(radian + -time * 0.4) * (radius + sin(-time * 4.0 + delay))
112            );
113          vec4 mvPosition = modelViewMatrix * vec4(updatePosition, 1.0
113);
114          float distanceFromCamera = length(mvPosition.xyz);
115          float pointSize = 1000.0 / distanceFromCamera * 0.55;
116          vMix = delay / 6.2831853;
117          gl_Position = projectionMatrix * mvPosition;
118          gl_PointSize = pointSize;
119        }
120      `,
121      fragmentShader: `
122        precision highp float;
123        uniform vec3 colorA;
124        uniform vec3 colorB;
125        varying float vMix;
126        void main() {
127          vec2 p = gl_PointCoord * 2.0 - 1.0;
128          float r = length(p);
129          float opacity1 = (1.0 - smoothstep(0.5, 0.7, r));
130          float opacity2 = smoothstep(0.8, 1.0, r) * (1.0 - smoothstep(1.0, 1.2, r));
131          vec3 col = mix(colorA, colorB, vMix);
132          gl_FragColor = vec4(col, (opacity1 + opacity2) * 0.0633);
133        }
134      `
135    });
136    const points = new THREE.Points(geometry, material);
137    scene.add(points);
138
139    // ——— mouse interaction ———
140    const mouseNDC = new THREE.Vector2(0, 0);
141    let interactionStarted = false;
142    function onMove(e) {
143      const rect = container.getBoundingClientRect();
144      mouseNDC.x = (e.clientX - rect.left) / rect.width * 2 - 1;
145      mouseNDC.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
146      interactionStarted = true;
147    }
148    window.addEventListener('mousemove', onMove);
149    const raycaster = new THREE.Raycaster();
150    const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
151    const intersection = new THREE.Vector3();
152    function smoother(t) {
153      return t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
154    }
155
156    // ——— animation loop ———
157    const clock = new THREE.Clock();
158    let raf = 0;
159    let running = true;
160    const positionsAttr = geometry.attributes.position.array;
161    const repellingRadius = 15.0;
162    const velocityFactor = 1.0;
163    function tick() {
164      if (!running) return;
165      uniforms.time.value = clock.getElapsedTime();
166      if (interactionStarted) {
167        raycaster.setFromCamera(mouseNDC, camera);
168        raycaster.ray.intersectPlane(plane, intersection);
169        for (let i = 0; i < positionsAttr.length; i += 3) {
170          const px = positionsAttr[i];
171          const py = positionsAttr[i + 1];
172          const pz = positionsAttr[i + 2];
173          const dxx = px - intersection.x;
174          const dyy = py - intersection.y;
175          const dzz = pz - intersection.z;
176          const dist = Math.sqrt(dxx * dxx + dyy * dyy + dzz * dzz) || 1e-6;
177          if (dist < repellingRadius) {
178            const dirX = dxx / dist;
179            const dirY = dyy / dist;
180            const dirZ = dzz / dist;
181            const ease = smoother(1 - dist / repellingRadius);
182            const strength = ease * velocityFactor;
183            velocities[i] += dirX * strength;
184            velocities[i + 1] += dirY * strength;
185            velocities[i + 2] += dirZ * strength;
186          }
187          positionsAttr[i] += velocities[i];
188          positionsAttr[i + 1] += velocities[i + 1];
189          positionsAttr[i + 2] += velocities[i + 2];
190          velocities[i] *= 0.95;
191          velocities[i + 1] *= 0.95;
192          velocities[i + 2] *= 0.95;
193          positionsAttr[i] += (initialPositions[i] - positionsAttr[i]) * 0.09;
194          positionsAttr[i + 1] += (initialPositions[i + 1] - positionsAttr[i + 1]) * 0.09;
195          positionsAttr[i + 2] += (initialPositions[i + 2] - positionsAttr[i + 2]) * 0.09;
196        }
197        geometry.attributes.position.needsUpdate = true;
198      }
199      renderer.render(scene, camera);
200      raf = requestAnimationFrame(tick);
201    }
202    tick();
203    return () => {
204      running = false;
205      cancelAnimationFrame(raf);
206      ro.disconnect();
207      window.removeEventListener('mousemove', onMove);
208      geometry.dispose();
209      material.dispose();
210      renderer.dispose();
211      if (renderer.domElement.parentNode) {
212        renderer.domElement.parentNode.removeChild(renderer.domElement);
213      }
214    };
215  }, [color]);
216  return /*#__PURE__*/React.createElement("div", {
217    ref: containerRef,
218    className: className,
219    style: {
220      position: 'absolute',
221      inset: 0,
222      ...style
223    },
224    "aria-hidden": "true"
225  });
226}
227window.DnaHelix = DnaHelix;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.