TKK_E32230273/js/particles.js

631 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// particles.js exact replication of antigravity.google background animation
import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
/* ─────────────────────────── CONFIG ─────────────────────────── */
const CONFIG = {
light: {
bg: '#ffffff',
color1: '#2c64ed',
color2: '#f84242',
color3: '#ffcf03'
},
dark: {
bg: '#121217',
color1: '#318bf7',
color2: '#bada4c',
color3: '#e35058'
},
gridSize: 256,
density: 150,
particlesScale: 1,
ringDisplacement: 0.15,
ringWidth: 0.05,
ringWidth2: 0.015,
cameraZoom: 3.5,
theme: 'light' // white background like antigravity.google default
};
/* ─────────────────────── SIMPLEX NOISE GLSL ─────────────────── */
const noiseGLSL = `
vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
vec4 permute(vec4 x){ return mod(((x*34.0)+1.0)*x, 289.0); }
float permute(float x){ return floor(mod(((x*34.0)+1.0)*x, 289.0)); }
vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }
float taylorInvSqrt(float r){ return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec2 v){
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
float snoise(vec3 v){
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy) );
vec3 x0 = v - i + dot(i, C.xxx) ;
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min( g.xyz, l.zxy );
vec3 i2 = max( g.xyz, l.zxy );
vec3 x1 = x0 - i1 + 1.0 * C.xxx;
vec3 x2 = x0 - i2 + 2.0 * C.xxx;
vec3 x3 = x0 - 1. + 3.0 * C.xxx;
i = mod(i, 289.0 );
vec4 p = permute( permute( permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
float n_ = 1.0/7.0;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z *ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_ );
vec4 x = x_ *ns.x + ns.yyyy;
vec4 y = y_ *ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4( x.xy, y.xy );
vec4 b1 = vec4( x.zw, y.zw );
vec4 s0 = floor(b0)*2.0 + 1.0;
vec4 s1 = floor(b1)*2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
vec3 p0 = vec3(a0.xy,h.x);
vec3 p1 = vec3(a0.zw,h.y);
vec3 p2 = vec3(a1.xy,h.z);
vec3 p3 = vec3(a1.zw,h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
dot(p2,x2), dot(p3,x3) ) );
}
`;
/* ───────────────── SIMULATION FRAGMENT SHADER ───────────────── */
const simFragmentShader = `
precision highp float;
uniform sampler2D uPosition;
uniform sampler2D uPosRefs;
uniform vec2 uMousePos;
uniform float uTime;
uniform float uDeltaTime;
uniform float uIsHovering;
vec2 hash( vec2 p ){
p = vec2( dot(p,vec2(2127.1,81.17)), dot(p,vec2(1269.5,283.37)) );
return fract(sin(p)*43758.5453);
}
void main() {
vec2 simTexCoords = gl_FragCoord.xy / vec2(${CONFIG.gridSize}.0, ${CONFIG.gridSize}.0);
vec4 pFrame = texture2D(uPosition, simTexCoords);
float scale = pFrame.z;
float velocity = pFrame.w;
vec2 refPos = texture2D(uPosRefs, simTexCoords).xy;
float seed = hash(simTexCoords).x;
float seed2 = hash(simTexCoords).y;
float time = uTime * .5;
float lifeEnd = 3. + sin(seed2 * 100.) * 1.;
float lifeTime = mod((seed * 100.) + time, lifeEnd);
vec2 disp = vec2(0., 0.);
vec2 pos = pFrame.xy;
float distRadius = 0.15;
vec2 targetPos = refPos;
vec2 direction = normalize(targetPos - pos);
direction *= .01;
float dist = length(targetPos - pos);
float distStrength = smoothstep(distRadius, 0., dist);
if(dist > 0.005){
pos += direction * distStrength;
}
if(lifeTime < .01){
pos = refPos;
pFrame.xy = refPos;
scale = 0.;
}
// Scale lifecycle
float targetScale = smoothstep(.01, 0.5, lifeTime) - smoothstep(0.5, 1., lifeTime/lifeEnd);
float scaleDiff = targetScale - scale;
scaleDiff *= .1;
scale += scaleDiff;
// Final position
vec2 finalPos = pos + (disp * smoothstep(0.001, distRadius, dist));
vec2 diff = finalPos - pFrame.xy;
diff *= .2;
velocity = 0.0;
vec4 frame = vec4(pFrame.xy + diff, scale, velocity);
gl_FragColor = frame;
}
`;
/* ─────────────── RENDER VERTEX SHADER ─────────────── */
const renderVertexShader = `
precision highp float;
attribute vec4 seeds;
uniform sampler2D uPosition;
uniform float uTime;
uniform float uParticleScale;
uniform float uPixelRatio;
uniform int uColorScheme;
uniform float uIsHovering;
uniform float uPulseProgress;
varying vec4 vSeeds;
varying float vVelocity;
varying vec2 vLocalPos;
varying vec2 vScreenPos;
varying float vScale;
${noiseGLSL}
void main() {
vec4 pos = texture2D(uPosition, uv);
vSeeds = seeds;
float noiseX = snoise(vec3( vec2(pos.xy * 10.), uTime * .2 + 100.));
float noiseY = snoise(vec3( vec2(pos.xy * 10.), uTime * .2));
float noiseX2 = snoise(vec3( vec2(pos.xy * .5), uTime * .15 + 45.));
float noiseY2 = snoise(vec3( vec2(pos.xy * .5), uTime * .15 + 87.));
// Pulse wave
float cDist = length(pos.xy) * 1.;
float progress = uPulseProgress;
float t = smoothstep(progress - .25, progress, cDist) - smoothstep(progress, progress + .25, cDist);
t *= smoothstep(1., .0, cDist);
pos.xy *= 1. + (t * .02);
float dist = smoothstep(0., 0.9, pos.w);
dist = mix(0., dist, uIsHovering);
pos.y += noiseY * 0.005 * dist;
pos.x += noiseX * 0.005 * dist;
pos.y += noiseY2 * 0.02;
pos.x += noiseX2 * 0.02;
vVelocity = pos.w;
vScale = pos.z;
vLocalPos = pos.xy;
vec4 viewSpace = modelViewMatrix * vec4(vec3(pos.xy, 0.), 1.0);
gl_Position = projectionMatrix * viewSpace;
vScreenPos = gl_Position.xy;
float minScale = .25;
minScale += float(uColorScheme) * .75;
gl_PointSize = ((vScale * 7.) * (uPixelRatio * 0.5) * uParticleScale) + (minScale * uPixelRatio);
}
`;
/* ─────────────── RENDER FRAGMENT SHADER ─────────────── */
const renderFragmentShader = `
precision highp float;
varying vec4 vSeeds;
varying vec2 vScreenPos;
varying vec2 vLocalPos;
varying float vScale;
varying float vVelocity;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
uniform vec2 uMousePos;
uniform vec2 uRez;
uniform float uAlpha;
uniform float uTime;
uniform int uColorScheme;
${noiseGLSL}
#define PI 3.1415926535897932384626433832795
float sdRoundBox( in vec2 p, in vec2 b, in vec4 r )
{
r.xy = (p.x>0.0)?r.xy : r.zw;
r.x = (p.y>0.0)?r.x : r.y;
vec2 q = abs(p)-b+r.x;
return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
vec2 rotate(vec2 v, float a) {
float s = sin(a);
float c = cos(a);
mat2 m = mat2(c, s, -s, c);
return m * v;
}
void main() {
float uBorderSize = 0.2;
vec2 center = vec2(.48, .4);
float ratio = uRez.x / uRez.y;
float angle = atan(vLocalPos.y - uMousePos.y, vLocalPos.x - uMousePos.x);
vec2 uv = gl_PointCoord.xy;
uv -= vec2(0.5);
uv.y *= -1.;
vec2 tuv = vScreenPos;
tuv = rotate(tuv, uTime * 1.);
tuv.y *= 1./ratio;
tuv += .5;
float h = 0.8;
float progress = vVelocity;
vec3 col = mix(mix(uColor1, uColor2, progress/h), mix(uColor2, uColor3, (progress - h)/(1.0 - h)), step(h, progress));
vec3 color = col;
float dist = sqrt(dot(uv, uv));
float dr = .5;
float t = smoothstep(dr+(uBorderSize + .0001), dr-uBorderSize, dist);
t = clamp(t, 0., 1.);
float rounded = sdRoundBox(uv, vec2(0.5, 0.2), vec4(.25));
rounded = smoothstep(.1, 0., rounded);
float disc = smoothstep(.5, .45, length(uv));
float a = uAlpha * disc * smoothstep(0.1, 0.2, vScale);
if(a < 0.01){
discard;
}
color = clamp(color, 0., 1.);
color = mix(color, color * clamp(vVelocity, 0., 1.), float(uColorScheme));
gl_FragColor = vec4(color, clamp(a, 0., 1.));
}
`;
/* ─────────────── POISSON DISK SAMPLING ─────────────── */
function linearMap(x, a, b, c, d) {
return ((x - a) * (d - c)) / (b - a) + c;
}
function poissonDiskSample(width, height, minDist, maxDist, tries = 20) {
const cellSize = minDist / Math.SQRT2;
const gridW = Math.ceil(width / cellSize);
const gridH = Math.ceil(height / cellSize);
const grid = new Int32Array(gridW * gridH).fill(-1);
const points = [];
const active = [];
function gridIdx(x, y) {
return Math.floor(y / cellSize) * gridW + Math.floor(x / cellSize);
}
function addPoint(x, y) {
const i = points.length;
points.push([x, y]);
active.push(i);
grid[gridIdx(x, y)] = i;
return i;
}
function inNeighbourhood(px, py) {
const gx = Math.floor(px / cellSize);
const gy = Math.floor(py / cellSize);
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
const nx = gx + dx, ny = gy + dy;
if (nx < 0 || ny < 0 || nx >= gridW || ny >= gridH) continue;
const idx = grid[ny * gridW + nx];
if (idx === -1) continue;
const [ex, ey] = points[idx];
const d = (px - ex) ** 2 + (py - ey) ** 2;
if (d < minDist * minDist) return true;
}
}
return false;
}
addPoint(width / 2, height / 2);
while (active.length > 0) {
const ri = Math.floor(Math.random() * active.length);
const [cx, cy] = points[active[ri]];
let found = false;
for (let t = 0; t < tries; t++) {
const angle = Math.random() * Math.PI * 2;
const dist = minDist + Math.random() * (maxDist - minDist);
const nx = cx + Math.cos(angle) * dist;
const ny = cy + Math.sin(angle) * dist;
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
if (!inNeighbourhood(nx, ny)) {
addPoint(nx, ny);
found = true;
break;
}
}
if (!found) active.splice(ri, 1);
}
return points;
}
/* ─────────────── MAIN INIT ─────────────── */
function initBackground() {
const canvas = document.getElementById('webgl-bg');
if (!canvas) return;
const theme = CONFIG.theme;
const colors = CONFIG[theme];
const colorScheme = theme === 'dark' ? 0 : 1;
const renderer = new THREE.WebGLRenderer({
canvas, alpha: true, antialias: true,
powerPreference: 'high-performance', stencil: false, precision: 'highp'
});
const pixelRatio = Math.min(window.devicePixelRatio, 2);
renderer.setPixelRatio(pixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
const scene = new THREE.Scene();
scene.background = new THREE.Color(colors.bg);
const camera = new THREE.PerspectiveCamera(
40, window.innerWidth / window.innerHeight, 0.1, 1000
);
camera.position.z = CONFIG.cameraZoom;
/* ── Create particle base positions with Poisson disk sampling ── */
const SIZE = CONFIG.gridSize; // 256
const count = SIZE * SIZE; // 65536
const minDist = linearMap(CONFIG.density, 0, 300, 10, 2);
const maxDist = linearMap(CONFIG.density, 0, 300, 11, 3);
const poissonPoints = poissonDiskSample(500, 500, minDist, maxDist, 20);
// Create position data texture from Poisson points
const posData = new Float32Array(count * 4);
for (let i = 0; i < count; i++) {
const pi = i % poissonPoints.length;
const pt = poissonPoints[pi];
posData[i * 4 + 0] = (pt[0] - 250) * (1 / 250);
posData[i * 4 + 1] = (pt[1] - 250) * (1 / 250);
posData[i * 4 + 2] = 0; // scale
posData[i * 4 + 3] = 0; // velocity
}
const posTex = new THREE.DataTexture(posData, SIZE, SIZE, THREE.RGBAFormat, THREE.FloatType);
posTex.needsUpdate = true;
const refTex = new THREE.DataTexture(posData.slice(), SIZE, SIZE, THREE.RGBAFormat, THREE.FloatType);
refTex.needsUpdate = true;
/* ── Render targets (GPGPU ping-pong) ── */
const rtParams = {
wrapS: THREE.ClampToEdgeWrapping,
wrapT: THREE.ClampToEdgeWrapping,
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
type: THREE.FloatType,
depthBuffer: false,
stencilBuffer: false
};
let rt1 = new THREE.WebGLRenderTarget(SIZE, SIZE, rtParams);
let rt2 = new THREE.WebGLRenderTarget(SIZE, SIZE, rtParams);
// Initialize render targets with position data
const initScene = new THREE.Scene();
const initCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const initMat = new THREE.ShaderMaterial({
uniforms: { uTex: { value: posTex } },
vertexShader: `void main(){ gl_Position = vec4(position, 1.0); }`,
fragmentShader: `
precision highp float;
uniform sampler2D uTex;
void main(){
vec2 uv = gl_FragCoord.xy / vec2(${SIZE}.0);
gl_FragColor = texture2D(uTex, uv);
}
`
});
initScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), initMat));
renderer.setRenderTarget(rt1);
renderer.render(initScene, initCamera);
renderer.setRenderTarget(rt2);
renderer.render(initScene, initCamera);
renderer.setRenderTarget(null);
/* ── Simulation material (GPGPU) ── */
const simMaterial = new THREE.ShaderMaterial({
uniforms: {
uPosition: { value: rt1.texture },
uPosRefs: { value: refTex },
uMousePos: { value: new THREE.Vector2(0, 0) },
uRingRadius: { value: 0.2 },
uDeltaTime: { value: 0 },
uRingWidth: { value: CONFIG.ringWidth },
uRingWidth2: { value: CONFIG.ringWidth2 },
uIsHovering: { value: 0 },
uRingDisplacement: { value: CONFIG.ringDisplacement },
uTime: { value: 0 }
},
vertexShader: `void main(){ gl_Position = vec4(position, 1.0); }`,
fragmentShader: simFragmentShader
});
const simScene = new THREE.Scene();
simScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), simMaterial));
const simCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
/* ── Particle render material ── */
const particleScale = (window.innerWidth / pixelRatio) / 2000 * CONFIG.particlesScale;
const particleGeo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const uvs = new Float32Array(count * 2);
const seeds = new Float32Array(count * 4);
for (let i = 0; i < count; i++) {
const col = i % SIZE;
const row = Math.floor(i / SIZE);
uvs[i * 2] = col / SIZE;
uvs[i * 2 + 1] = row / SIZE;
seeds[i * 4] = Math.random();
seeds[i * 4 + 1] = Math.random();
seeds[i * 4 + 2] = Math.random();
seeds[i * 4 + 3] = Math.random();
}
particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particleGeo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
particleGeo.setAttribute('seeds', new THREE.BufferAttribute(seeds, 4));
const renderMaterial = new THREE.ShaderMaterial({
uniforms: {
uPosition: { value: rt1.texture },
uTime: { value: 0 },
uColor1: { value: new THREE.Color(colors.color1) },
uColor2: { value: new THREE.Color(colors.color2) },
uColor3: { value: new THREE.Color(colors.color3) },
uAlpha: { value: 1.0 },
uIsHovering: { value: 0 },
uPulseProgress: { value: 0 },
uMousePos: { value: new THREE.Vector2(0, 0) },
uRez: { value: new THREE.Vector2(window.innerWidth * pixelRatio, window.innerHeight * pixelRatio) },
uParticleScale: { value: particleScale },
uPixelRatio: { value: pixelRatio },
uColorScheme: { value: colorScheme }
},
vertexShader: renderVertexShader,
fragmentShader: renderFragmentShader,
transparent: true,
depthTest: false,
depthWrite: false
});
const mesh = new THREE.Points(particleGeo, renderMaterial);
mesh.scale.set(5, -5, 5);
scene.add(mesh);
/* ── Mouse interaction ── */
const mousePos = new THREE.Vector2(0, 0);
let pulseProgress = 0;
let pulseActive = false;
let pulseStart = 0;
window.addEventListener('pointermove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) / rect.width * 2 - 1;
const my = -((e.clientY - rect.top) / rect.height) * 2 + 1;
mousePos.set(mx * 0.175, my * 0.175);
});
/* ── Animation loop ── */
const clock = new THREE.Clock();
let lastTime = 0;
function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
const dt = elapsed - lastTime;
lastTime = elapsed;
// Pulse wave
if (pulseActive) {
pulseProgress = (elapsed - pulseStart) / 2;
if (pulseProgress > 1) {
pulseActive = false;
pulseProgress = 0;
}
}
// Update simulation uniforms
simMaterial.uniforms.uPosition.value = rt1.texture;
simMaterial.uniforms.uTime.value = elapsed;
simMaterial.uniforms.uDeltaTime.value = dt;
simMaterial.uniforms.uRingRadius.value = 0.175 + Math.sin(elapsed) * 0.03 + Math.cos(elapsed * 3) * 0.02;
simMaterial.uniforms.uMousePos.value = mousePos;
// GPGPU simulation pass
renderer.setRenderTarget(rt2);
renderer.render(simScene, simCamera);
renderer.setRenderTarget(null);
// Update render uniforms
renderMaterial.uniforms.uPosition.value = rt2.texture;
renderMaterial.uniforms.uTime.value = elapsed;
renderMaterial.uniforms.uMousePos.value = mousePos;
renderMaterial.uniforms.uPulseProgress.value = pulseProgress;
// Render particles
renderer.autoClear = false;
renderer.clear();
renderer.render(scene, camera);
// Ping-pong swap
const tmp = rt1;
rt1 = rt2;
rt2 = tmp;
}
animate();
// Trigger initial pulse
pulseActive = true;
pulseStart = clock.getElapsedTime();
/* ── Resize handler ── */
window.addEventListener('resize', () => {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderMaterial.uniforms.uRez.value.set(w * pixelRatio, h * pixelRatio);
renderMaterial.uniforms.uPixelRatio.value = pixelRatio;
renderMaterial.uniforms.uParticleScale.value = (w / pixelRatio) / 2000 * CONFIG.particlesScale;
});
}
document.addEventListener('DOMContentLoaded', initBackground);