Code snippet ported from the artist @yuruyurau. Check out his stuff!
import * as THREE from "three";
const scene = new THREE.Scene();
scene.background = null; // transparent to show CSS gradient
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 2000);
camera.position.set(1.1, 364.0, -0.6);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, canvas: document.querySelector("canvas")! });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
const N = 12000;
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(N * 3), 3));
geometry.setAttribute("color", new THREE.BufferAttribute(new Float32Array(N * 3), 3));
geometry.setAttribute("size", new THREE.BufferAttribute(new Float32Array(N), 1));
const material = new THREE.ShaderMaterial({
uniforms: { opacity: { value: 0.85 } },
vertexShader: `
attribute float size;
attribute vec3 color;
varying float vDepth;
varying vec3 vColor;
void main() {
vColor = color;
vec4 mv = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (450.0 / -mv.z);
gl_Position = projectionMatrix * mv;
vDepth = -mv.z;
}`,
fragmentShader: `
uniform float opacity;
varying float vDepth;
varying vec3 vColor;
void main() {
float d = length(gl_PointCoord - 0.5);
if (d > 0.5) discard;
float a = opacity * smoothstep(0.5, 0.0, d) * smoothstep(600.0, 100.0, vDepth);
gl_FragColor = vec4(vColor * a, a);
}`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
scene.add(new THREE.Points(geometry, material));
const tmpColor = new THREE.Color();
let t = 0;
function animate() {
requestAnimationFrame(animate);
t += Math.PI / 120;
const pos = geometry.attributes.position.array as Float32Array;
const col = geometry.attributes.color.array as Float32Array;
const siz = geometry.attributes.size.array as Float32Array;
for (let i = 0; i < N; i++) {
const y = i / 345;
const k = (y < 11 ? 6 + Math.sin((y | 0) ^ 8) * 6 : y / 5 + Math.cos(y / 2)) * Math.cos(i - t / 4);
const e = y / 7 - 13;
const d = Math.hypot(k, e) + Math.sin(e / 4 + t) / 2;
const angle = d / 2 + 1 - t / 2;
const q = y * k / d * (3 + Math.sin(d * 2 + y / 2 - t * 4));
const r = 60 + q;
pos[i * 3] = r * Math.cos(angle);
pos[i * 3 + 1] = d * 29 - 170;
pos[i * 3 + 2] = r * Math.sin(angle);
const hue = (y / 30 + Math.sin(angle * 2) * 0.1 + d * 0.02 + t / 4) % 1;
tmpColor.setHSL(hue < 0 ? hue + 1 : hue, 0.9, 0.55 + Math.sin(d + t * 2) * 0.1);
col[i * 3] = tmpColor.r;
col[i * 3 + 1] = tmpColor.g;
col[i * 3 + 2] = tmpColor.b;
siz[i] = 5.5 + Math.sin(d + t) * 1.2;
}
geometry.attributes.position.needsUpdate = true;
geometry.attributes.color.needsUpdate = true;
geometry.attributes.size.needsUpdate = true;
renderer.render(scene, camera);
}
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
animate();