Skip to content

Instantly share code, notes, and snippets.

@aldoyh
Last active July 22, 2025 20:00
Show Gist options
  • Save aldoyh/eaa65baa399c885391b1e3a7f07c6bda to your computer and use it in GitHub Desktop.
Save aldoyh/eaa65baa399c885391b1e3a7f07c6bda to your computer and use it in GitHub Desktop.
[threejs/webgl] ❍ Yet another Metaballs Hero Section with ThreeJS
<section class="section hero-section">
<div id="container"></div>
<div id="stats"></div>
<div id="ui-container"></div>
<div class="header-area">
<div class="logo-container" id="theme-toggle">
<div class="logo-circles">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
</div>
</div>
<div class="center-logo">
<h1 id="logo-text">Nexus.</h1>
</div>
</div>
<div class="hero">
<h1>Where matter becomes<br>thought and thought<br>becomes form</h1>
<h2 id="story-text">
our vessel drifts at coordinates (0.00, 0.00)<br>
gravitational field extends 0.10 units into quantum foam<br>
currently merging with 0 other entities
</h2>
</div>
<div class="contact-info">
<p class="contact-heading">+Get in touch</p>
<span class="contact-email">[email protected]</span>
</div>
<div class="footer-links">
<a href="#" class="footer-link">Fluid Dynamics</a>
<a href="#" class="footer-link">Organic Shapes</a>
<a href="#" class="footer-link">Interactive Forms</a>
<a href="#" class="footer-link">Motion Studies</a>
<a href="#" class="footer-link">Contact</a>
</div>
<div class="coordinates">
<p>Nexus State • Active</p>
<p>where consciousness flows</p>
</div>
</section>
<section class="section fin-section">
<div class="fin-text">{ Fin }</div>
</section>
import * as THREE from "https://esm.sh/[email protected]";
import { Pane } from "https://cdn.skypack.dev/[email protected]";
let scene, camera, renderer, material;
let clock,
mouse = { x: 0, y: 0 };
let cursorSphere3D = new THREE.Vector3(0, 0, 0);
let activeMerges = 0;
let targetMousePosition = new THREE.Vector2(0.5, 0.5);
let mousePosition = new THREE.Vector2(0.5, 0.5);
let lastTime = performance.now();
let frameCount = 0;
let fps = 0;
// Enhanced device detection
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const isLowPowerDevice = isMobile || navigator.hardwareConcurrency <= 4;
const devicePixelRatio = Math.min(
window.devicePixelRatio || 1,
isMobile ? 1.5 : 2
);
const presets = {
moody: {
sphereCount: isMobile ? 4 : 6,
ambientIntensity: 0.02,
diffuseIntensity: 0.6,
specularIntensity: 1.8,
specularPower: 8,
fresnelPower: 1.2,
backgroundColor: new THREE.Color(0x050505),
sphereColor: new THREE.Color(0x000000),
lightColor: new THREE.Color(0xffffff),
lightPosition: new THREE.Vector3(1, 1, 1),
smoothness: 0.3,
contrast: 2.0,
fogDensity: 0.12,
cursorGlowIntensity: 0.4,
cursorGlowRadius: 1.2,
cursorGlowColor: new THREE.Color(0xffffff)
},
cosmic: {
sphereCount: isMobile ? 5 : 8,
ambientIntensity: 0.03,
diffuseIntensity: 0.8,
specularIntensity: 1.6,
specularPower: 6,
fresnelPower: 1.4,
backgroundColor: new THREE.Color(0x000011),
sphereColor: new THREE.Color(0x000022),
lightColor: new THREE.Color(0x88aaff),
lightPosition: new THREE.Vector3(0.5, 1, 0.5),
smoothness: 0.4,
contrast: 2.0,
fogDensity: 0.15,
cursorGlowIntensity: 0.8,
cursorGlowRadius: 1.5,
cursorGlowColor: new THREE.Color(0x4477ff)
},
minimal: {
sphereCount: isMobile ? 2 : 3,
ambientIntensity: 0.0,
diffuseIntensity: 0.25,
specularIntensity: 1.3,
specularPower: 11,
fresnelPower: 1.7,
backgroundColor: new THREE.Color(0x0a0a0a),
sphereColor: new THREE.Color(0x000000),
lightColor: new THREE.Color(0xffffff),
lightPosition: new THREE.Vector3(1, 0.5, 0.8),
smoothness: 0.25,
contrast: 2.0,
fogDensity: 0.1,
cursorGlowIntensity: 0.3,
cursorGlowRadius: 1.0,
cursorGlowColor: new THREE.Color(0xffffff)
},
vibrant: {
sphereCount: isMobile ? 6 : 10,
ambientIntensity: 0.05,
diffuseIntensity: 0.9,
specularIntensity: 1.5,
specularPower: 5,
fresnelPower: 1.3,
backgroundColor: new THREE.Color(0x0a0505),
sphereColor: new THREE.Color(0x110000),
lightColor: new THREE.Color(0xff8866),
lightPosition: new THREE.Vector3(0.8, 1.2, 0.6),
smoothness: 0.5,
contrast: 2.0,
fogDensity: 0.08,
cursorGlowIntensity: 0.8,
cursorGlowRadius: 1.3,
cursorGlowColor: new THREE.Color(0xff6644)
},
neon: {
sphereCount: isMobile ? 4 : 7,
ambientIntensity: 0.04,
diffuseIntensity: 1.0,
specularIntensity: 2.0,
specularPower: 4,
fresnelPower: 1.0,
backgroundColor: new THREE.Color(0x000505),
sphereColor: new THREE.Color(0x000808),
lightColor: new THREE.Color(0x00ffcc),
lightPosition: new THREE.Vector3(0.7, 1.3, 0.8),
smoothness: 0.7,
contrast: 2.0,
fogDensity: 0.08,
cursorGlowIntensity: 0.8,
cursorGlowRadius: 1.4,
cursorGlowColor: new THREE.Color(0x00ffaa)
},
sunset: {
sphereCount: isMobile ? 3 : 5,
ambientIntensity: 0.04,
diffuseIntensity: 0.7,
specularIntensity: 1.4,
specularPower: 7,
fresnelPower: 1.5,
backgroundColor: new THREE.Color(0x150505),
sphereColor: new THREE.Color(0x100000),
lightColor: new THREE.Color(0xff6622),
lightPosition: new THREE.Vector3(1.2, 0.4, 0.6),
smoothness: 0.35,
contrast: 2.0,
fogDensity: 0.1,
cursorGlowIntensity: 0.8,
cursorGlowRadius: 1.4,
cursorGlowColor: new THREE.Color(0xff4422)
},
midnight: {
sphereCount: isMobile ? 3 : 4,
ambientIntensity: 0.01,
diffuseIntensity: 0.4,
specularIntensity: 1.6,
specularPower: 9,
fresnelPower: 1.8,
backgroundColor: new THREE.Color(0x000010),
sphereColor: new THREE.Color(0x000015),
lightColor: new THREE.Color(0x4466ff),
lightPosition: new THREE.Vector3(0.9, 0.8, 1.0),
smoothness: 0.28,
contrast: 2.0,
fogDensity: 0.14,
cursorGlowIntensity: 0.8,
cursorGlowRadius: 1.6,
cursorGlowColor: new THREE.Color(0x3355ff)
},
toxic: {
sphereCount: isMobile ? 5 : 9,
ambientIntensity: 0.06,
diffuseIntensity: 0.85,
specularIntensity: 1.7,
specularPower: 6,
fresnelPower: 1.1,
backgroundColor: new THREE.Color(0x001000),
sphereColor: new THREE.Color(0x001500),
lightColor: new THREE.Color(0x66ff44),
lightPosition: new THREE.Vector3(0.6, 1.1, 0.7),
smoothness: 0.55,
contrast: 2.0,
fogDensity: 0.09,
cursorGlowIntensity: 0.8,
cursorGlowRadius: 1.7,
cursorGlowColor: new THREE.Color(0x44ff22)
},
pastel: {
sphereCount: isMobile ? 4 : 6,
ambientIntensity: 0.08,
diffuseIntensity: 0.5,
specularIntensity: 1.2,
specularPower: 12,
fresnelPower: 2.0,
backgroundColor: new THREE.Color(0x101018),
sphereColor: new THREE.Color(0x080814),
lightColor: new THREE.Color(0xaabbff),
lightPosition: new THREE.Vector3(1.0, 0.7, 0.9),
smoothness: 0.38,
contrast: 1.8,
fogDensity: 0.07,
cursorGlowIntensity: 0.35,
cursorGlowRadius: 1.1,
cursorGlowColor: new THREE.Color(0x8899ff)
},
dithered: {
sphereCount: isMobile ? 5 : 8,
ambientIntensity: 0.1,
diffuseIntensity: 0.8,
specularIntensity: 1.5,
specularPower: 6,
fresnelPower: 1.2,
backgroundColor: new THREE.Color(0x0a0520),
sphereColor: new THREE.Color(0x000000),
lightColor: new THREE.Color(0xff00ff),
lightPosition: new THREE.Vector3(0.8, 0.8, 0.8),
smoothness: 0.6,
contrast: 1.8,
fogDensity: 0.05,
cursorGlowIntensity: 1.0,
cursorGlowRadius: 2.0,
cursorGlowColor: new THREE.Color(0x00ffff)
},
holographic: {
sphereCount: isMobile ? 4 : 6,
ambientIntensity: 0.12,
diffuseIntensity: 1.2,
specularIntensity: 2.5,
specularPower: 3,
fresnelPower: 0.8,
backgroundColor: new THREE.Color(0x0a0a15),
sphereColor: new THREE.Color(0x050510),
lightColor: new THREE.Color(0xccaaff),
lightPosition: new THREE.Vector3(0.9, 0.9, 1.2),
smoothness: 0.8,
contrast: 1.6,
fogDensity: 0.06,
cursorGlowIntensity: 1.2,
cursorGlowRadius: 2.2,
cursorGlowColor: new THREE.Color(0xaa77ff)
}
};
const settings = {
preset: "holographic",
...presets.holographic,
fixedTopLeftRadius: 0.8,
fixedBottomRightRadius: 0.9,
smallTopLeftRadius: 0.3,
smallBottomRightRadius: 0.35,
cursorRadiusMin: 0.08,
cursorRadiusMax: 0.15,
animationSpeed: 0.6,
movementScale: 1.2,
mouseSmoothness: 0.1,
mergeDistance: 1.5,
mouseProximityEffect: true,
minMovementScale: 0.3,
maxMovementScale: 1.0
};
function getStoryText(x, y, radius, merges, fps) {
if (isMobile) {
return `vessel: (${x}, ${y})<br>field: ${radius}u<br>merges: ${merges}<br>flux: ${fps}hz`;
}
return `our vessel drifts at coordinates (${x}, ${y})<br>gravitational field extends ${radius} units into quantum foam<br>currently merging with ${merges} other entities<br>temporal flux: ${fps} cycles per second`;
}
init();
animate();
function init() {
const container = document.getElementById("container");
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
camera.position.z = 1;
clock = new THREE.Clock();
renderer = new THREE.WebGLRenderer({
antialias: !isMobile && !isLowPowerDevice,
alpha: true,
powerPreference: isMobile ? "default" : "high-performance",
preserveDrawingBuffer: false,
premultipliedAlpha: false
});
// Fixed pixel ratio handling
const pixelRatio = Math.min(devicePixelRatio, isMobile ? 1.5 : 2);
renderer.setPixelRatio(pixelRatio);
// Get actual viewport dimensions
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
renderer.setSize(viewportWidth, viewportHeight);
renderer.setClearColor(0x000000, 0);
renderer.outputColorSpace = THREE.SRGBColorSpace;
// Enhanced canvas styling
const canvas = renderer.domElement;
canvas.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 0 !important;
display: block !important;
`;
container.appendChild(canvas);
// Create material with improved shader
material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uResolution: { value: new THREE.Vector2(viewportWidth, viewportHeight) },
uActualResolution: {
value: new THREE.Vector2(
viewportWidth * pixelRatio,
viewportHeight * pixelRatio
)
},
uPixelRatio: { value: pixelRatio },
uMousePosition: { value: new THREE.Vector2(0.5, 0.5) },
uCursorSphere: { value: new THREE.Vector3(0, 0, 0) },
uCursorRadius: { value: settings.cursorRadiusMin },
uSphereCount: { value: settings.sphereCount },
uFixedTopLeftRadius: { value: settings.fixedTopLeftRadius },
uFixedBottomRightRadius: { value: settings.fixedBottomRightRadius },
uSmallTopLeftRadius: { value: settings.smallTopLeftRadius },
uSmallBottomRightRadius: { value: settings.smallBottomRightRadius },
uMergeDistance: { value: settings.mergeDistance },
uSmoothness: { value: settings.smoothness },
uAmbientIntensity: { value: settings.ambientIntensity },
uDiffuseIntensity: { value: settings.diffuseIntensity },
uSpecularIntensity: { value: settings.specularIntensity },
uSpecularPower: { value: settings.specularPower },
uFresnelPower: { value: settings.fresnelPower },
uBackgroundColor: { value: settings.backgroundColor },
uSphereColor: { value: settings.sphereColor },
uLightColor: { value: settings.lightColor },
uLightPosition: { value: settings.lightPosition },
uContrast: { value: settings.contrast },
uFogDensity: { value: settings.fogDensity },
uAnimationSpeed: { value: settings.animationSpeed },
uMovementScale: { value: settings.movementScale },
uMouseProximityEffect: { value: settings.mouseProximityEffect },
uMinMovementScale: { value: settings.minMovementScale },
uMaxMovementScale: { value: settings.maxMovementScale },
uCursorGlowIntensity: { value: settings.cursorGlowIntensity },
uCursorGlowRadius: { value: settings.cursorGlowRadius },
uCursorGlowColor: { value: settings.cursorGlowColor },
uIsSafari: { value: isSafari ? 1.0 : 0.0 },
uIsMobile: { value: isMobile ? 1.0 : 0.0 },
uIsLowPower: { value: isLowPowerDevice ? 1.0 : 0.0 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
${
isMobile || isSafari || isLowPowerDevice
? "precision mediump float;"
: "precision highp float;"
}
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uActualResolution;
uniform float uPixelRatio;
uniform vec2 uMousePosition;
uniform vec3 uCursorSphere;
uniform float uCursorRadius;
uniform int uSphereCount;
uniform float uFixedTopLeftRadius;
uniform float uFixedBottomRightRadius;
uniform float uSmallTopLeftRadius;
uniform float uSmallBottomRightRadius;
uniform float uMergeDistance;
uniform float uSmoothness;
uniform float uAmbientIntensity;
uniform float uDiffuseIntensity;
uniform float uSpecularIntensity;
uniform float uSpecularPower;
uniform float uFresnelPower;
uniform vec3 uBackgroundColor;
uniform vec3 uSphereColor;
uniform vec3 uLightColor;
uniform vec3 uLightPosition;
uniform float uContrast;
uniform float uFogDensity;
uniform float uAnimationSpeed;
uniform float uMovementScale;
uniform bool uMouseProximityEffect;
uniform float uMinMovementScale;
uniform float uMaxMovementScale;
uniform float uCursorGlowIntensity;
uniform float uCursorGlowRadius;
uniform vec3 uCursorGlowColor;
uniform float uIsSafari;
uniform float uIsMobile;
uniform float uIsLowPower;
varying vec2 vUv;
const float PI = 3.14159265359;
const float EPSILON = 0.001;
const float MAX_DIST = 100.0;
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
// Fixed coordinate transformation
vec3 screenToWorld(vec2 normalizedPos) {
// normalizedPos is already 0-1, convert to -1 to 1
vec2 uv = normalizedPos * 2.0 - 1.0;
// Apply aspect ratio correction
uv.x *= uResolution.x / uResolution.y;
return vec3(uv * 2.0, 0.0);
}
float getDistanceToCenter(vec2 pos) {
float dist = length(pos - vec2(0.5, 0.5)) * 2.0;
return smoothstep(0.0, 1.0, dist);
}
float sceneSDF(vec3 pos) {
float result = MAX_DIST;
// Fixed sphere positions using consistent coordinate system
vec3 topLeftPos = screenToWorld(vec2(0.08, 0.92));
float topLeft = sdSphere(pos - topLeftPos, uFixedTopLeftRadius);
vec3 smallTopLeftPos = screenToWorld(vec2(0.25, 0.72));
float smallTopLeft = sdSphere(pos - smallTopLeftPos, uSmallTopLeftRadius);
vec3 bottomRightPos = screenToWorld(vec2(0.92, 0.08));
float bottomRight = sdSphere(pos - bottomRightPos, uFixedBottomRightRadius);
vec3 smallBottomRightPos = screenToWorld(vec2(0.72, 0.25));
float smallBottomRight = sdSphere(pos - smallBottomRightPos, uSmallBottomRightRadius);
float t = uTime * uAnimationSpeed;
float dynamicMovementScale = uMovementScale;
if (uMouseProximityEffect) {
float distToCenter = getDistanceToCenter(uMousePosition);
float mixFactor = smoothstep(0.0, 1.0, distToCenter);
dynamicMovementScale = mix(uMinMovementScale, uMaxMovementScale, mixFactor);
}
// Optimized iterations for performance
int maxIter = uIsMobile > 0.5 ? 4 : (uIsLowPower > 0.5 ? 6 : min(uSphereCount, 10));
for (int i = 0; i < 10; i++) {
if (i >= uSphereCount || i >= maxIter) break;
float fi = float(i);
float speed = 0.4 + fi * 0.12;
float radius = 0.12 + mod(fi, 3.0) * 0.06;
float orbitRadius = (0.3 + mod(fi, 3.0) * 0.15) * dynamicMovementScale;
float phaseOffset = fi * PI * 0.35;
float distToCursor = length(vec3(0.0) - uCursorSphere);
float proximityScale = 1.0 + (1.0 - smoothstep(0.0, 1.0, distToCursor)) * 0.5;
orbitRadius *= proximityScale;
vec3 offset;
if (i == 0) {
offset = vec3(
sin(t * speed) * orbitRadius * 0.7,
sin(t * 0.5) * orbitRadius,
cos(t * speed * 0.7) * orbitRadius * 0.5
);
} else if (i == 1) {
offset = vec3(
sin(t * speed + PI) * orbitRadius * 0.5,
-sin(t * 0.5) * orbitRadius,
cos(t * speed * 0.7 + PI) * orbitRadius * 0.5
);
} else {
offset = vec3(
sin(t * speed + phaseOffset) * orbitRadius * 0.8,
cos(t * speed * 0.85 + phaseOffset * 1.3) * orbitRadius * 0.6,
sin(t * speed * 0.5 + phaseOffset) * 0.3
);
}
vec3 toCursor = uCursorSphere - offset;
float cursorDist = length(toCursor);
if (cursorDist < uMergeDistance && cursorDist > 0.0) {
float attraction = (1.0 - cursorDist / uMergeDistance) * 0.3;
offset += normalize(toCursor) * attraction;
}
float movingSphere = sdSphere(pos - offset, radius);
float blend = 0.05;
if (cursorDist < uMergeDistance) {
float influence = 1.0 - (cursorDist / uMergeDistance);
blend = mix(0.05, uSmoothness, influence * influence * influence);
}
result = smin(result, movingSphere, blend);
}
float cursorBall = sdSphere(pos - uCursorSphere, uCursorRadius);
float topLeftGroup = smin(topLeft, smallTopLeft, 0.4);
float bottomRightGroup = smin(bottomRight, smallBottomRight, 0.4);
result = smin(result, topLeftGroup, 0.3);
result = smin(result, bottomRightGroup, 0.3);
result = smin(result, cursorBall, uSmoothness);
return result;
}
vec3 calcNormal(vec3 p) {
float eps = uIsLowPower > 0.5 ? 0.002 : 0.001;
return normalize(vec3(
sceneSDF(p + vec3(eps, 0, 0)) - sceneSDF(p - vec3(eps, 0, 0)),
sceneSDF(p + vec3(0, eps, 0)) - sceneSDF(p - vec3(0, eps, 0)),
sceneSDF(p + vec3(0, 0, eps)) - sceneSDF(p - vec3(0, 0, eps))
));
}
float ambientOcclusion(vec3 p, vec3 n) {
if (uIsLowPower > 0.5) {
float h1 = sceneSDF(p + n * 0.03);
float h2 = sceneSDF(p + n * 0.06);
float occ = (0.03 - h1) + (0.06 - h2) * 0.5;
return clamp(1.0 - occ * 2.0, 0.0, 1.0);
} else {
float occ = 0.0;
float weight = 1.0;
for (int i = 0; i < 6; i++) {
float dist = 0.01 + 0.015 * float(i * i);
float h = sceneSDF(p + n * dist);
occ += (dist - h) * weight;
weight *= 0.85;
}
return clamp(1.0 - occ, 0.0, 1.0);
}
}
float softShadow(vec3 ro, vec3 rd, float mint, float maxt, float k) {
if (uIsLowPower > 0.5) {
float result = 1.0;
float t = mint;
for (int i = 0; i < 3; i++) {
t += 0.3;
if (t >= maxt) break;
float h = sceneSDF(ro + rd * t);
if (h < EPSILON) return 0.0;
result = min(result, k * h / t);
}
return result;
} else {
float result = 1.0;
float t = mint;
for (int i = 0; i < 20; i++) {
if (t >= maxt) break;
float h = sceneSDF(ro + rd * t);
if (h < EPSILON) return 0.0;
result = min(result, k * h / t);
t += h;
}
return result;
}
}
float rayMarch(vec3 ro, vec3 rd) {
float t = 0.0;
int maxSteps = uIsMobile > 0.5 ? 16 : (uIsSafari > 0.5 ? 16 : 48);
for (int i = 0; i < 48; i++) {
if (i >= maxSteps) break;
vec3 p = ro + rd * t;
float d = sceneSDF(p);
if (d < EPSILON) {
return t;
}
if (t > 5.0) {
break;
}
t += d * (uIsLowPower > 0.5 ? 1.2 : 0.9);
}
return -1.0;
}
vec3 lighting(vec3 p, vec3 rd, float t) {
if (t < 0.0) {
return vec3(0.0);
}
vec3 normal = calcNormal(p);
vec3 viewDir = -rd;
vec3 baseColor = uSphereColor;
float ao = ambientOcclusion(p, normal);
vec3 ambient = uLightColor * uAmbientIntensity * ao;
vec3 lightDir = normalize(uLightPosition);
float diff = max(dot(normal, lightDir), 0.0);
float shadow = softShadow(p, lightDir, 0.01, 10.0, 20.0);
vec3 diffuse = uLightColor * diff * uDiffuseIntensity * shadow;
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), uSpecularPower);
float fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), uFresnelPower);
vec3 specular = uLightColor * spec * uSpecularIntensity * fresnel;
vec3 fresnelRim = uLightColor * fresnel * 0.4;
float distToCursor = length(p - uCursorSphere);
if (distToCursor < uCursorRadius + 0.4) {
float highlight = 1.0 - smoothstep(0.0, uCursorRadius + 0.4, distToCursor);
specular += uLightColor * highlight * 0.2;
float glow = exp(-distToCursor * 3.0) * 0.15;
ambient += uLightColor * glow * 0.5;
}
vec3 color = (baseColor + ambient + diffuse + specular + fresnelRim) * ao;
color = pow(color, vec3(uContrast * 0.9));
color = color / (color + vec3(0.8));
return color;
}
float calculateCursorGlow(vec3 worldPos) {
float dist = length(worldPos.xy - uCursorSphere.xy);
float glow = 1.0 - smoothstep(0.0, uCursorGlowRadius, dist);
glow = pow(glow, 2.0);
return glow * uCursorGlowIntensity;
}
void main() {
vec2 uv = (gl_FragCoord.xy * 2.0 - uResolution.xy) / uResolution.xy;
uv.x *= uResolution.x / uResolution.y;
vec3 ro = vec3(uv * 2.0, -1.0);
vec3 rd = vec3(0.0, 0.0, 1.0);
float t = rayMarch(ro, rd);
vec3 p = ro + rd * t;
vec3 color = lighting(p, rd, t);
float cursorGlow = calculateCursorGlow(ro);
vec3 glowContribution = uCursorGlowColor * cursorGlow;
if (t > 0.0) {
float fogAmount = 1.0 - exp(-t * uFogDensity);
color = mix(color, uBackgroundColor.rgb, fogAmount * 0.3);
color += glowContribution * 0.3;
gl_FragColor = vec4(color, 1.0);
} else {
if (cursorGlow > 0.01) {
gl_FragColor = vec4(glowContribution, cursorGlow * 0.8);
} else {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
}
}
}
`,
transparent: true
});
const geometry = new THREE.PlaneGeometry(2, 2);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
setupUI();
setupEventListeners();
// Initialize cursor position
onPointerMove({
clientX: window.innerWidth / 2,
clientY: window.innerHeight / 2
});
}
function setupEventListeners() {
window.addEventListener("mousemove", onPointerMove, { passive: true });
window.addEventListener("touchstart", onTouchStart, { passive: false });
window.addEventListener("touchmove", onTouchMove, { passive: false });
window.addEventListener("touchend", onTouchEnd, { passive: false });
window.addEventListener("resize", onWindowResize, { passive: true });
window.addEventListener(
"orientationchange",
() => {
setTimeout(onWindowResize, 100);
},
{ passive: true }
);
}
function onTouchStart(event) {
event.preventDefault();
if (event.touches.length > 0) {
const touch = event.touches[0];
onPointerMove({
clientX: touch.clientX,
clientY: touch.clientY
});
}
}
function onTouchMove(event) {
event.preventDefault();
if (event.touches.length > 0) {
const touch = event.touches[0];
onPointerMove({
clientX: touch.clientX,
clientY: touch.clientY
});
}
}
function onTouchEnd(event) {
event.preventDefault();
}
function onPointerMove(event) {
// Use consistent coordinate system for mouse
targetMousePosition.x = event.clientX / window.innerWidth;
targetMousePosition.y = 1.0 - event.clientY / window.innerHeight;
// Convert to world coordinates using the same system as fixed spheres
const normalizedX = targetMousePosition.x;
const normalizedY = targetMousePosition.y;
const worldPos = screenToWorldJS(normalizedX, normalizedY);
cursorSphere3D.copy(worldPos);
// Calculate merges and radius
let closestDistance = 1000.0;
activeMerges = 0;
// Use consistent positioning for merge detection
const fixedPositions = [
screenToWorldJS(0.08, 0.92), // top left
screenToWorldJS(0.25, 0.72), // small top left
screenToWorldJS(0.92, 0.08), // bottom right
screenToWorldJS(0.72, 0.25) // small bottom right
];
fixedPositions.forEach((pos) => {
const dist = cursorSphere3D.distanceTo(pos);
closestDistance = Math.min(closestDistance, dist);
if (dist < settings.mergeDistance) activeMerges++;
});
const proximityFactor = Math.max(
0,
1.0 - closestDistance / settings.mergeDistance
);
const smoothFactor =
proximityFactor * proximityFactor * (3.0 - 2.0 * proximityFactor);
const dynamicRadius =
settings.cursorRadiusMin +
(settings.cursorRadiusMax - settings.cursorRadiusMin) * smoothFactor;
material.uniforms.uCursorSphere.value.copy(cursorSphere3D);
material.uniforms.uCursorRadius.value = dynamicRadius;
updateStory(
cursorSphere3D.x,
cursorSphere3D.y,
dynamicRadius,
activeMerges,
fps
);
}
// JavaScript version of screenToWorld for consistency
function screenToWorldJS(normalizedX, normalizedY) {
const uv_x = normalizedX * 2.0 - 1.0;
const uv_y = normalizedY * 2.0 - 1.0;
const aspect = window.innerWidth / window.innerHeight;
return new THREE.Vector3(uv_x * aspect * 2.0, uv_y * 2.0, 0.0);
}
function updateStory(x, y, radius, merges, fps) {
const storyText = document.getElementById("story-text");
if (storyText) {
const newText = getStoryText(
x.toFixed(2),
y.toFixed(2),
radius.toFixed(2),
merges,
fps || 0
);
storyText.innerHTML = newText;
}
}
function applyPreset(presetName) {
const preset = presets[presetName];
if (!preset) return;
settings.preset = presetName;
Object.keys(preset).forEach((key) => {
if (settings.hasOwnProperty(key)) {
settings[key] = preset[key];
}
});
// Update uniforms
material.uniforms.uSphereCount.value = settings.sphereCount;
material.uniforms.uAmbientIntensity.value = settings.ambientIntensity;
material.uniforms.uDiffuseIntensity.value = settings.diffuseIntensity;
material.uniforms.uSpecularIntensity.value = settings.specularIntensity;
material.uniforms.uSpecularPower.value = settings.specularPower;
material.uniforms.uFresnelPower.value = settings.fresnelPower;
material.uniforms.uBackgroundColor.value = settings.backgroundColor;
material.uniforms.uSphereColor.value = settings.sphereColor;
material.uniforms.uLightColor.value = settings.lightColor;
material.uniforms.uLightPosition.value = settings.lightPosition;
material.uniforms.uSmoothness.value = settings.smoothness;
material.uniforms.uContrast.value = settings.contrast;
material.uniforms.uFogDensity.value = settings.fogDensity;
material.uniforms.uCursorGlowIntensity.value = settings.cursorGlowIntensity;
material.uniforms.uCursorGlowRadius.value = settings.cursorGlowRadius;
material.uniforms.uCursorGlowColor.value = settings.cursorGlowColor;
}
function setupUI() {
const uiContainer = document.getElementById("ui-container");
if (!uiContainer) return;
const pane = new Pane({
container: uiContainer,
title: "Metaball Controls",
expanded: !isMobile
});
pane
.addBinding(settings, "preset", {
options: {
Moody: "moody",
Cosmic: "cosmic",
Minimal: "minimal",
Vibrant: "vibrant",
Neon: "neon",
Sunset: "sunset",
Midnight: "midnight",
Toxic: "toxic",
Pastel: "pastel",
Psychedelic: "dithered",
Holographic: "holographic"
}
})
.on("change", (ev) => {
applyPreset(ev.value);
pane.refresh();
});
const metaballFolder = pane.addFolder({ title: "Metaballs" });
metaballFolder
.addBinding(settings, "fixedTopLeftRadius", {
min: 0.2,
max: 2.0,
step: 0.01,
label: "Top Left Size"
})
.on("change", (ev) => {
material.uniforms.uFixedTopLeftRadius.value = ev.value;
});
metaballFolder
.addBinding(settings, "fixedBottomRightRadius", {
min: 0.2,
max: 2.0,
step: 0.01,
label: "Bottom Right Size"
})
.on("change", (ev) => {
material.uniforms.uFixedBottomRightRadius.value = ev.value;
});
metaballFolder
.addBinding(settings, "smallTopLeftRadius", {
min: 0.1,
max: 0.8,
step: 0.01,
label: "Small Top Left"
})
.on("change", (ev) => {
material.uniforms.uSmallTopLeftRadius.value = ev.value;
});
metaballFolder
.addBinding(settings, "smallBottomRightRadius", {
min: 0.1,
max: 0.8,
step: 0.01,
label: "Small Bottom Right"
})
.on("change", (ev) => {
material.uniforms.uSmallBottomRightRadius.value = ev.value;
});
metaballFolder
.addBinding(settings, "sphereCount", {
min: 2,
max: 10,
step: 1,
label: "Moving Count"
})
.on("change", (ev) => {
material.uniforms.uSphereCount.value = ev.value;
});
metaballFolder
.addBinding(settings, "smoothness", {
min: 0.1,
max: 1.0,
step: 0.01,
label: "Blend Smoothness"
})
.on("change", (ev) => {
material.uniforms.uSmoothness.value = ev.value;
});
const mouseFolder = pane.addFolder({ title: "Mouse Interaction" });
mouseFolder
.addBinding(settings, "mouseProximityEffect")
.on("change", (ev) => {
material.uniforms.uMouseProximityEffect.value = ev.value;
});
mouseFolder
.addBinding(settings, "minMovementScale", {
min: 0.1,
max: 1.0,
step: 0.05
})
.on("change", (ev) => {
material.uniforms.uMinMovementScale.value = ev.value;
});
mouseFolder
.addBinding(settings, "maxMovementScale", {
min: 0.5,
max: 2.0,
step: 0.05
})
.on("change", (ev) => {
material.uniforms.uMaxMovementScale.value = ev.value;
});
mouseFolder.addBinding(settings, "mouseSmoothness", {
min: 0.01,
max: 0.2,
step: 0.01,
label: "Mouse Smoothness"
});
const cursorFolder = pane.addFolder({ title: "Cursor" });
cursorFolder.addBinding(settings, "cursorRadiusMin", {
min: 0.05,
max: 0.2,
step: 0.01,
label: "Min Radius"
});
cursorFolder.addBinding(settings, "cursorRadiusMax", {
min: 0.1,
max: 0.25,
step: 0.01,
label: "Max Radius"
});
const animationFolder = pane.addFolder({ title: "Animation" });
animationFolder
.addBinding(settings, "animationSpeed", {
min: 0.1,
max: 3.0,
step: 0.1
})
.on("change", (ev) => {
material.uniforms.uAnimationSpeed.value = ev.value;
});
animationFolder
.addBinding(settings, "movementScale", {
min: 0.5,
max: 2.0,
step: 0.1
})
.on("change", (ev) => {
material.uniforms.uMovementScale.value = ev.value;
});
const lightingFolder = pane.addFolder({ title: "Lighting" });
lightingFolder
.addBinding(settings, "ambientIntensity", {
min: 0,
max: 0.5,
step: 0.01
})
.on("change", (ev) => {
material.uniforms.uAmbientIntensity.value = ev.value;
});
lightingFolder
.addBinding(settings, "diffuseIntensity", {
min: 0,
max: 1.0,
step: 0.01
})
.on("change", (ev) => {
material.uniforms.uDiffuseIntensity.value = ev.value;
});
lightingFolder
.addBinding(settings, "specularIntensity", {
min: 0,
max: 2.0,
step: 0.01
})
.on("change", (ev) => {
material.uniforms.uSpecularIntensity.value = ev.value;
});
lightingFolder
.addBinding(settings, "specularPower", {
min: 1,
max: 64,
step: 1
})
.on("change", (ev) => {
material.uniforms.uSpecularPower.value = ev.value;
});
lightingFolder
.addBinding(settings, "fresnelPower", {
min: 1,
max: 5,
step: 0.1
})
.on("change", (ev) => {
material.uniforms.uFresnelPower.value = ev.value;
});
lightingFolder
.addBinding(settings, "contrast", {
min: 0.5,
max: 2.0,
step: 0.1
})
.on("change", (ev) => {
material.uniforms.uContrast.value = ev.value;
});
const glowFolder = pane.addFolder({ title: "Cursor Glow" });
glowFolder
.addBinding(settings, "cursorGlowIntensity", {
min: 0,
max: 2.0,
step: 0.1
})
.on("change", (ev) => {
material.uniforms.uCursorGlowIntensity.value = ev.value;
});
glowFolder
.addBinding(settings, "cursorGlowRadius", {
min: 0.5,
max: 3.0,
step: 0.1
})
.on("change", (ev) => {
material.uniforms.uCursorGlowRadius.value = ev.value;
});
glowFolder
.addBinding(settings, "fogDensity", {
min: 0,
max: 0.5,
step: 0.01
})
.on("change", (ev) => {
material.uniforms.uFogDensity.value = ev.value;
});
}
function onWindowResize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
// Update resolution uniforms
material.uniforms.uResolution.value.set(width, height);
material.uniforms.uActualResolution.value.set(
width * devicePixelRatio,
height * devicePixelRatio
);
// Ensure canvas stays properly positioned
const canvas = renderer.domElement;
canvas.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 0 !important;
display: block !important;
`;
if (renderer.info) {
renderer.info.autoReset = true;
}
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
const currentTime = performance.now();
frameCount++;
if (currentTime - lastTime >= 1000) {
fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
updateStory(
cursorSphere3D.x,
cursorSphere3D.y,
material.uniforms.uCursorRadius.value,
activeMerges,
fps
);
frameCount = 0;
lastTime = currentTime;
}
// Smooth mouse movement
mousePosition.x +=
(targetMousePosition.x - mousePosition.x) * settings.mouseSmoothness;
mousePosition.y +=
(targetMousePosition.y - mousePosition.y) * settings.mouseSmoothness;
material.uniforms.uTime.value = clock.getElapsedTime();
material.uniforms.uMousePosition.value = mousePosition;
// Periodic cleanup for better memory management
if (performance.now() % 5000 < 16) {
renderer.renderLists.dispose();
}
renderer.render(scene, camera);
}
// Email functionality
document.addEventListener("DOMContentLoaded", function () {
const emailLink = document.querySelector(".contact-email");
if (emailLink) {
const originalText = emailLink.textContent;
emailLink.addEventListener("click", function (e) {
e.preventDefault();
navigator.clipboard
.writeText("[email protected]")
.then(() => {
emailLink.textContent = "transmission sent to clipboard";
setTimeout(() => {
emailLink.textContent = originalText;
}, 2000);
})
.catch(() => {
window.location.href = "mailto:[email protected]";
});
});
}
});
@import url("https://fonts.cdnfonts.com/css/pp-neue-montreal");
@font-face {
font-family: "PPSupplyMono";
src: url("https://assets.codepen.io/7558/PPSupplyMono-Regular.ttf")
format("truetype");
font-weight: normal;
font-style: normal;
font-display: swap;
}
:root {
--warm-off-white: #ffffff;
--warm-off-white-dim: #cccccc;
--text-primary: #ffffff;
--text-secondary: #cccccc;
--background-dark: #1a1a1a;
--background-gradient-1: #222222;
--background-gradient-2: #1a1a1a;
--font-primary: "PP Neue Montreal", sans-serif;
--font-secondary: "PPSupplyMono", monospace;
--font-sans: "PP Neue Montreal", sans-serif;
--font-size-small: 10px;
--font-size-regular: 1rem;
--font-size-medium: 1.5rem;
--font-size-large: 4rem;
--spacing-small: 0.5rem;
--spacing-medium: 1rem;
--spacing-large: 2rem;
--transition-fast: 0.3s ease;
--transition-medium: 0.5s cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
*,
*:before,
*:after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
@media (min-width: 768px) {
html {
font-size: 18px;
}
}
@media (min-width: 1200px) {
html {
font-size: 20px;
}
}
body {
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--text-primary);
background-color: var(--background-dark);
font-family: var(--font-secondary);
font-size: var(--font-size-small);
}
.section {
width: 100vw;
height: 100vh;
position: relative;
}
.hero-section {
padding: var(--spacing-large);
}
.fin-section {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--background-dark);
z-index: 20;
position: relative;
}
.fin-text {
font-family: var(--font-primary);
font-size: 3rem;
color: var(--text-secondary);
text-transform: none;
letter-spacing: 0.05em;
}
body::after {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("https://img.freepik.com/premium-photo/white-dust-scratches-black-background_279525-2.jpg?w=640");
background-repeat: repeat;
opacity: 0.2;
mix-blend-mode: multiply;
pointer-events: none;
z-index: 1;
filter: invert(0);
}
#container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #0f0f0f;
z-index: 0;
pointer-events: none;
}
#ui-container {
position: fixed;
top: 10px;
right: 10px;
z-index: 100;
opacity: 0.8;
transition: opacity var(--transition-fast);
}
#ui-container:hover {
opacity: 1;
}
#stats {
position: fixed;
top: 10px;
left: 10px;
z-index: 100;
}
.header-area {
position: fixed;
top: var(--spacing-large);
left: 0;
width: 100%;
padding: 0 var(--spacing-large);
display: flex;
justify-content: center;
z-index: 10;
}
.logo-container {
position: absolute;
left: var(--spacing-large);
top: 0;
display: flex;
align-items: center;
height: 2rem;
z-index: 10;
cursor: pointer;
}
.logo-circles {
position: relative;
width: 100%;
height: 100%;
}
.circle {
position: absolute;
border-radius: 50%;
transition: transform var(--transition-medium);
width: 1.4rem;
height: 1.4rem;
background-color: var(--text-primary);
top: 50%;
}
.circle-1 {
left: 0;
transform: translate(0, -50%);
}
.circle-2 {
left: 0.8rem;
transform: translate(0, -50%);
mix-blend-mode: exclusion;
}
.logo-container:hover .circle-1 {
transform: translate(-0.5rem, -50%);
}
.logo-container:hover .circle-2 {
transform: translate(0.5rem, -50%);
}
.center-logo {
text-align: center;
z-index: 10;
height: 2rem;
}
#logo-text {
font-family: var(--font-primary);
font-weight: 400;
font-size: var(--font-size-medium);
line-height: 1;
margin: 0;
color: var(--text-primary);
text-transform: none;
}
.hero {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
z-index: 10;
color: var(--text-primary);
width: 90%;
max-width: 800px;
}
.hero h1 {
font-family: var(--font-primary);
font-weight: 400;
font-size: var(--font-size-large);
line-height: 0.9;
letter-spacing: -0.02em;
color: var(--text-primary);
text-transform: none;
margin-bottom: 2rem;
margin: 0 0 2rem 0;
}
.hero h2 {
font-family: var(--font-secondary);
font-size: var(--font-size-small);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1.4;
opacity: 0.7;
transition: opacity var(--transition-fast);
font-weight: normal;
margin: 0;
}
.hero:hover h2 {
opacity: 1;
}
.contact-info {
position: fixed;
top: 50%;
left: var(--spacing-large);
transform: translateY(-50%);
z-index: 10;
font-family: var(--font-secondary);
letter-spacing: 0.05em;
font-size: var(--font-size-small);
color: var(--text-primary);
text-transform: uppercase;
}
.contact-heading {
font-size: var(--font-size-small);
color: var(--text-secondary);
margin-bottom: var(--spacing-small);
}
.contact-email {
display: block;
color: var(--text-primary);
text-decoration: none;
transition: all var(--transition-fast);
cursor: pointer;
}
.contact-email:hover {
color: var(--text-secondary);
}
.footer-links {
position: fixed;
bottom: var(--spacing-large);
left: var(--spacing-large);
z-index: 10;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
font-family: var(--font-sans);
font-weight: 400;
font-size: var(--font-size-regular);
}
.footer-link {
color: var(--text-secondary);
text-decoration: none;
transition: all var(--transition-fast);
position: relative;
padding-left: 0;
text-transform: none;
font-size: 1rem;
}
.footer-link::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 0;
height: 1px;
background-color: var(--text-primary);
transform: translateY(-50%);
transition: width var(--transition-fast), opacity var(--transition-fast);
opacity: 0;
}
.footer-link:hover {
color: var(--text-primary);
padding-left: 1.2rem;
}
.footer-link:hover::before {
width: 0.8rem;
opacity: 1;
}
.coordinates {
position: fixed;
bottom: var(--spacing-large);
right: var(--spacing-large);
text-align: right;
z-index: 10;
font-family: var(--font-secondary);
font-size: var(--font-size-small);
color: var(--text-secondary);
}
.tp-dfwv {
min-width: 280px !important;
}
.section {
transition: all 0.6s ease;
}
@media (max-width: 768px) {
.hero h1 {
font-size: 3rem;
}
.footer-links {
gap: 0.4rem;
}
.coordinates {
font-size: 10px;
}
.hero p {
font-size: 10px;
}
.story-display {
width: 90%;
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.hero-section {
padding: var(--spacing-medium);
}
.header-area,
.contact-info,
.footer-links,
.coordinates {
padding: 0 var(--spacing-medium);
}
.logo-container,
.contact-info,
.footer-links {
left: var(--spacing-medium);
}
.coordinates {
right: var(--spacing-medium);
}
.hero h1 {
font-size: 2.2rem;
}
.circle {
width: 1.2rem;
height: 1.2rem;
}
.circle-2 {
left: 0.7rem;
}
#logo-text {
font-size: 1.3rem;
}
.contact-heading {
font-size: 10px;
}
.contact-email {
font-size: 10px;
}
.story-display {
width: 95%;
font-size: 0.75rem;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment