Globe Particles
Scope
- Apply only to a globe-like 3D particle visualization.
- Do not change full page layout, copy, or unrelated motion systems.
- Use for planetary, orbital, infrastructure, or synthesized data-globe effects.
- Keep the core neutral or white-hot and derive ring/glow accents from the design's primary color.
Visual Target
- Dense spherical core of luminous points.
- Thinner outer orbital ring or flattened disc around the sphere.
- Clear globe silhouette with tilt, depth, and layered particle density.
- Dark atmospheric background, restrained glow, clean structure, and subtle sci-fi depth.
- Premium and cinematic, not playful or noisy.
HTML And CSS
<div class="globe-particles-shell">
<canvas class="globe-particles-canvas" data-globe-particles></canvas>
</div>
.globe-particles-shell {
position: relative;
width: min(100%, 760px);
aspect-ratio: 1 / 1;
}
.globe-particles-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
pointer-events: none;
}
Particle Shader
Use circular shader points so particles stay crisp and luminous.
const globeParticleVertex = `
attribute float a_size;
attribute float a_layer;
uniform float u_time;
uniform float u_pointSize;
varying float v_layer;
varying float v_depth;
varying float v_falloff;
void main() {
vec3 pos = position;
float breathe = 1.0 + sin(u_time * 0.65 + a_layer * 4.0) * 0.012;
pos *= breathe;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = u_pointSize * a_size * (1.0 / max(0.18, -mvPosition.z));
gl_Position = projectionMatrix * mvPosition;
v_layer = a_layer;
v_depth = smoothstep(-1.8, 1.8, pos.z);
v_falloff = smoothstep(2.45, 0.25, length(pos));
}
`;
const globeParticleFragment = `
precision highp float;
uniform vec3 u_coreColor;
uniform vec3 u_accentColor;
varying float v_layer;
varying float v_depth;
varying float v_falloff;
void main() {
vec2 uv = gl_PointCoord - 0.5;
float d = length(uv);
float alpha = smoothstep(0.5, 0.0, d);
alpha *= alpha;
vec3 color = mix(u_coreColor, u_accentColor, smoothstep(0.35, 1.0, v_layer));
color += vec3(1.0) * v_depth * 0.08;
color = mix(color * 0.42, color, clamp(v_falloff + v_layer * 0.28, 0.0, 1.0));
alpha *= mix(0.52, 1.0, clamp(v_falloff + v_layer * 0.24, 0.0, 1.0));
gl_FragColor = vec4(color, alpha);
}
`;
Three.js Recipe
import * as THREE from "three";
function hexToRgb01(hex) {
const clean = hex.replace("#", "").trim();
const value = clean.length === 3
? clean.split("").map((char) => char + char).join("")
: clean;
return new THREE.Color(
parseInt(value.slice(0, 2), 16) / 255,
parseInt(value.slice(2, 4), 16) / 255,
parseInt(value.slice(4, 6), 16) / 255
);
}
function buildGlobeParticleGeometry(options = {}) {
const sphereCount = options.sphereCount || 2600;
const ringCount = options.ringCount || 1300;
const radius = options.radius || 1.35;
const ringRadius = options.ringRadius || 2.05;
const ringThickness = options.ringThickness || 0.12;
const total = sphereCount + ringCount;
const positions = new Float32Array(total * 3);
const sizes = new Float32Array(total);
const layers = new Float32Array(total);
for (let i = 0; i < sphereCount; i++) {
const z = Math.random() * 2 - 1;
const theta = Math.random() * Math.PI * 2;
const r = radius * (0.58 + Math.pow(Math.random(), 0.42) * 0.42);
const root = Math.sqrt(1 - z * z);
const index = i * 3;
positions[index] = Math.cos(theta) * root * r;
positions[index + 1] = Math.sin(theta) * root * r;
positions[index + 2] = z * r;
sizes[i] = 0.72 + Math.random() * 0.72;
layers[i] = Math.random() * 0.28;
}
for (let i = 0; i < ringCount; i++) {
const pointIndex = sphereCount + i;
const angle = Math.random() * Math.PI * 2;
const r = ringRadius + (Math.random() - 0.5) * ringThickness;
const y = (Math.random() - 0.5) * ringThickness * 0.58;
const index = pointIndex * 3;
positions[index] = Math.cos(angle) * r;
positions[index + 1] = y;
positions[index + 2] = Math.sin(angle) * r;
sizes[pointIndex] = 0.62 + Math.random() * 0.58;
layers[pointIndex] = 0.72 + Math.random() * 0.28;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("a_size", new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute("a_layer", new THREE.BufferAttribute(layers, 1));
return geometry;
}
function initGlobeParticles(canvas, options = {}) {
if (!canvas) return () => {};
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true,
});
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.6));
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);
camera.position.set(0, 0, options.cameraDistance || 5.6);
const accent = options.accentColor
? new THREE.Color(options.accentColor)
: hexToRgb01(getComputedStyle(document.documentElement).getPropertyValue("--brand-accent").trim() || "#8b5cf6");
const geometry = buildGlobeParticleGeometry(options);
const material = new THREE.ShaderMaterial({
vertexShader: globeParticleVertex,
fragmentShader: globeParticleFragment,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
uniforms: {
u_time: { value: 0 },
u_pointSize: { value: options.pointSize || 18 },
u_coreColor: { value: new THREE.Color(options.coreColor || 0xf8fafc) },
u_accentColor: { value: accent },
},
});
const particles = new THREE.Points(geometry, material);
particles.rotation.x = options.tiltX ?? -0.42;
particles.rotation.z = options.tiltZ ?? 0.22;
scene.add(particles);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const pointer = new THREE.Vector2(0, 0);
let rafId = 0;
function resize() {
const width = Math.max(1, canvas.clientWidth);
const height = Math.max(1, canvas.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.6));
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function handlePointerMove(event) {
const rect = canvas.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
pointer.y = ((event.clientY - rect.top) / rect.height - 0.5) * 2;
}
function render(time = 0) {
const t = time * 0.001;
material.uniforms.u_time.value = t;
const mouseStrength = options.mouseStrength ?? 0.08;
const breath = reduceMotion ? 0 : Math.sin(t * 0.55) * 0.045;
particles.rotation.y = t * (options.rotationSpeed || 0.12);
particles.rotation.x = (options.tiltX ?? -0.42) + pointer.y * mouseStrength;
particles.rotation.z = (options.tiltZ ?? 0.22) + pointer.x * mouseStrength;
particles.scale.setScalar(1 + breath);
renderer.render(scene, camera);
if (!reduceMotion) rafId = requestAnimationFrame(render);
}
function handleResize() {
cancelAnimationFrame(rafId);
resize();
render();
}
resize();
render();
window.addEventListener("resize", handleResize);
window.addEventListener("pointermove", handlePointerMove);
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener("resize", handleResize);
window.removeEventListener("pointermove", handlePointerMove);
geometry.dispose();
material.dispose();
renderer.dispose();
};
}
const cleanupGlobe = initGlobeParticles(document.querySelector("[data-globe-particles]"), {
sphereCount: 2600,
ringCount: 1300,
accentColor: "#8b5cf6",
radius: 1.35,
ringRadius: 2.05,
rotationSpeed: 0.12,
mouseStrength: 0.08,
});
Tuning Knobs
- Density: tune
sphereCount and ringCount separately.
- Scale: tune
radius, ringRadius, ringThickness, and cameraDistance.
- Color: keep
coreColor neutral; derive accentColor from the brand primary.
- Motion: tune
rotationSpeed, tiltX, tiltZ, mouseStrength, and breathing amplitude.
- Glow: tune
pointSize, additive blending, and particle count so the shape stays crisp.
- Performance: lower particle counts or cap
maxDpr before changing the visual structure.
Taste Rules
- The silhouette must read as a globe, not a loose starfield.
- The ring should feel orbital and tilted, not like a flat decorative underline.
- Use restrained glow; let density and depth create the premium feel.
- Keep mouse response gentle so the object drifts rather than swings.
- Put the globe over a dark background or inside a dark atmospheric shell.
Avoid
- Generic starfield noise with no spherical structure.
- Oversized particles or bloom that destroys the globe silhouette.
- Hardcoded accent colors when the design has a clear primary color.
- Wild cursor interaction or fast spinning.
- Dense fog that turns the object into a blurry blob.
Quick Checks
- Sphere and ring are distinct particle populations.
- Core reads mostly neutral or white-hot.
- Accent color appears on ring, highlights, or glow.
- Tilt reveals the ring and globe depth.
- Reduced motion renders a still or near-still object.
- Geometry, material, renderer, listeners, and RAF are cleaned up.
1---2name: globe-particles3description: Create a globe-like 3D particle visualization with a dense luminous spherical core and thinner orbital ring or flattened disc. Use when a design needs a premium planetary, orbital, synthesized data-globe effect rendered with real WebGL/Three.js particles, not generic starfields or full page layout changes.4---56# Globe Particles78## Scope9- Apply only to a globe-like 3D particle visualization.10- Do not change full page layout, copy, or unrelated motion systems.11- Use for planetary, orbital, infrastructure, or synthesized data-globe effects.12- Keep the core neutral or white-hot and derive ring/glow accents from the design's primary color.1314## Visual Target15- Dense spherical core of luminous points.16- Thinner outer orbital ring or flattened disc around the sphere.17- Clear globe silhouette with tilt, depth, and layered particle density.18- Dark atmospheric background, restrained glow, clean structure, and subtle sci-fi depth.19- Premium and cinematic, not playful or noisy.2021## HTML And CSS2223```html24<div class="globe-particles-shell">25 <canvas class="globe-particles-canvas" data-globe-particles></canvas>26</div>27```2829```css30.globe-particles-shell {31 position: relative;32 width: min(100%, 760px);33 aspect-ratio: 1 / 1;34}3536.globe-particles-canvas {37 position: absolute;38 inset: 0;39 width: 100%;40 height: 100%;41 display: block;42 pointer-events: none;43}44```4546## Particle Shader47Use circular shader points so particles stay crisp and luminous.4849```js50const globeParticleVertex = `51attribute float a_size;52attribute float a_layer;5354uniform float u_time;55uniform float u_pointSize;5657varying float v_layer;58varying float v_depth;59varying float v_falloff;6061void main() {62 vec3 pos = position;63 float breathe = 1.0 + sin(u_time * 0.65 + a_layer * 4.0) * 0.012;64 pos *= breathe;6566 vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);67 gl_PointSize = u_pointSize * a_size * (1.0 / max(0.18, -mvPosition.z));68 gl_Position = projectionMatrix * mvPosition;6970 v_layer = a_layer;71 v_depth = smoothstep(-1.8, 1.8, pos.z);72 v_falloff = smoothstep(2.45, 0.25, length(pos));73}74`;7576const globeParticleFragment = `77precision highp float;7879uniform vec3 u_coreColor;80uniform vec3 u_accentColor;8182varying float v_layer;83varying float v_depth;84varying float v_falloff;8586void main() {87 vec2 uv = gl_PointCoord - 0.5;88 float d = length(uv);89 float alpha = smoothstep(0.5, 0.0, d);90 alpha *= alpha;9192 vec3 color = mix(u_coreColor, u_accentColor, smoothstep(0.35, 1.0, v_layer));93 color += vec3(1.0) * v_depth * 0.08;94 color = mix(color * 0.42, color, clamp(v_falloff + v_layer * 0.28, 0.0, 1.0));95 alpha *= mix(0.52, 1.0, clamp(v_falloff + v_layer * 0.24, 0.0, 1.0));9697 gl_FragColor = vec4(color, alpha);98}99`;100```101102## Three.js Recipe103104```js105import * as THREE from "three";106107function hexToRgb01(hex) {108 const clean = hex.replace("#", "").trim();109 const value = clean.length === 3110 ? clean.split("").map((char) => char + char).join("")111 : clean;112113 return new THREE.Color(114 parseInt(value.slice(0, 2), 16) / 255,115 parseInt(value.slice(2, 4), 16) / 255,116 parseInt(value.slice(4, 6), 16) / 255117 );118}119120function buildGlobeParticleGeometry(options = {}) {121 const sphereCount = options.sphereCount || 2600;122 const ringCount = options.ringCount || 1300;123 const radius = options.radius || 1.35;124 const ringRadius = options.ringRadius || 2.05;125 const ringThickness = options.ringThickness || 0.12;126 const total = sphereCount + ringCount;127128 const positions = new Float32Array(total * 3);129 const sizes = new Float32Array(total);130 const layers = new Float32Array(total);131132 for (let i = 0; i < sphereCount; i++) {133 const z = Math.random() * 2 - 1;134 const theta = Math.random() * Math.PI * 2;135 const r = radius * (0.58 + Math.pow(Math.random(), 0.42) * 0.42);136 const root = Math.sqrt(1 - z * z);137 const index = i * 3;138139 positions[index] = Math.cos(theta) * root * r;140 positions[index + 1] = Math.sin(theta) * root * r;141 positions[index + 2] = z * r;142 sizes[i] = 0.72 + Math.random() * 0.72;143 layers[i] = Math.random() * 0.28;144 }145146 for (let i = 0; i < ringCount; i++) {147 const pointIndex = sphereCount + i;148 const angle = Math.random() * Math.PI * 2;149 const r = ringRadius + (Math.random() - 0.5) * ringThickness;150 const y = (Math.random() - 0.5) * ringThickness * 0.58;151 const index = pointIndex * 3;152153 positions[index] = Math.cos(angle) * r;154 positions[index + 1] = y;155 positions[index + 2] = Math.sin(angle) * r;156 sizes[pointIndex] = 0.62 + Math.random() * 0.58;157 layers[pointIndex] = 0.72 + Math.random() * 0.28;158 }159160 const geometry = new THREE.BufferGeometry();161 geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));162 geometry.setAttribute("a_size", new THREE.BufferAttribute(sizes, 1));163 geometry.setAttribute("a_layer", new THREE.BufferAttribute(layers, 1));164 return geometry;165}166167function initGlobeParticles(canvas, options = {}) {168 if (!canvas) return () => {};169170 const renderer = new THREE.WebGLRenderer({171 canvas,172 antialias: true,173 alpha: true,174 });175 renderer.setClearColor(0x000000, 0);176 renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.6));177 renderer.outputColorSpace = THREE.SRGBColorSpace;178179 const scene = new THREE.Scene();180 const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);181 camera.position.set(0, 0, options.cameraDistance || 5.6);182183 const accent = options.accentColor184 ? new THREE.Color(options.accentColor)185 : hexToRgb01(getComputedStyle(document.documentElement).getPropertyValue("--brand-accent").trim() || "#8b5cf6");186187 const geometry = buildGlobeParticleGeometry(options);188 const material = new THREE.ShaderMaterial({189 vertexShader: globeParticleVertex,190 fragmentShader: globeParticleFragment,191 transparent: true,192 depthWrite: false,193 blending: THREE.AdditiveBlending,194 uniforms: {195 u_time: { value: 0 },196 u_pointSize: { value: options.pointSize || 18 },197 u_coreColor: { value: new THREE.Color(options.coreColor || 0xf8fafc) },198 u_accentColor: { value: accent },199 },200 });201202 const particles = new THREE.Points(geometry, material);203 particles.rotation.x = options.tiltX ?? -0.42;204 particles.rotation.z = options.tiltZ ?? 0.22;205 scene.add(particles);206207 const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;208 const pointer = new THREE.Vector2(0, 0);209 let rafId = 0;210211 function resize() {212 const width = Math.max(1, canvas.clientWidth);213 const height = Math.max(1, canvas.clientHeight);214 renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.6));215 renderer.setSize(width, height, false);216 camera.aspect = width / height;217 camera.updateProjectionMatrix();218 }219220 function handlePointerMove(event) {221 const rect = canvas.getBoundingClientRect();222 pointer.x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;223 pointer.y = ((event.clientY - rect.top) / rect.height - 0.5) * 2;224 }225226 function render(time = 0) {227 const t = time * 0.001;228 material.uniforms.u_time.value = t;229230 const mouseStrength = options.mouseStrength ?? 0.08;231 const breath = reduceMotion ? 0 : Math.sin(t * 0.55) * 0.045;232 particles.rotation.y = t * (options.rotationSpeed || 0.12);233 particles.rotation.x = (options.tiltX ?? -0.42) + pointer.y * mouseStrength;234 particles.rotation.z = (options.tiltZ ?? 0.22) + pointer.x * mouseStrength;235 particles.scale.setScalar(1 + breath);236237 renderer.render(scene, camera);238 if (!reduceMotion) rafId = requestAnimationFrame(render);239 }240241 function handleResize() {242 cancelAnimationFrame(rafId);243 resize();244 render();245 }246247 resize();248 render();249 window.addEventListener("resize", handleResize);250 window.addEventListener("pointermove", handlePointerMove);251252 return () => {253 cancelAnimationFrame(rafId);254 window.removeEventListener("resize", handleResize);255 window.removeEventListener("pointermove", handlePointerMove);256 geometry.dispose();257 material.dispose();258 renderer.dispose();259 };260}261262const cleanupGlobe = initGlobeParticles(document.querySelector("[data-globe-particles]"), {263 sphereCount: 2600,264 ringCount: 1300,265 accentColor: "#8b5cf6",266 radius: 1.35,267 ringRadius: 2.05,268 rotationSpeed: 0.12,269 mouseStrength: 0.08,270});271```272273## Tuning Knobs274- Density: tune `sphereCount` and `ringCount` separately.275- Scale: tune `radius`, `ringRadius`, `ringThickness`, and `cameraDistance`.276- Color: keep `coreColor` neutral; derive `accentColor` from the brand primary.277- Motion: tune `rotationSpeed`, `tiltX`, `tiltZ`, `mouseStrength`, and breathing amplitude.278- Glow: tune `pointSize`, additive blending, and particle count so the shape stays crisp.279- Performance: lower particle counts or cap `maxDpr` before changing the visual structure.280281## Taste Rules282- The silhouette must read as a globe, not a loose starfield.283- The ring should feel orbital and tilted, not like a flat decorative underline.284- Use restrained glow; let density and depth create the premium feel.285- Keep mouse response gentle so the object drifts rather than swings.286- Put the globe over a dark background or inside a dark atmospheric shell.287288## Avoid289- Generic starfield noise with no spherical structure.290- Oversized particles or bloom that destroys the globe silhouette.291- Hardcoded accent colors when the design has a clear primary color.292- Wild cursor interaction or fast spinning.293- Dense fog that turns the object into a blurry blob.294295## Quick Checks296- Sphere and ring are distinct particle populations.297- Core reads mostly neutral or white-hot.298- Accent color appears on ring, highlights, or glow.299- Tilt reveals the ring and globe depth.300- Reduced motion renders a still or near-still object.301- Geometry, material, renderer, listeners, and RAF are cleaned up.