Back to Components
Morphing Particle Sphere – Interactive 3D Text Animation
Component

Morphing Particle Sphere – Interactive 3D Text Animation

CodewithLord
November 22, 2025

This component is an interactive 3D particle animation using Three.js and GSAP. A sphere made of thousands of particles morphs dynamically into text input by the user and then returns to its spherical shape. The animation is highly visual, responsive, and uses color gradients to give depth and vibrancy to the particles.

Husky Dog Animation CSS

This component is an interactive 3D particle animation using Three.js and GSAP. A sphere made of thousands of particles morphs dynamically into text input by the user and then returns to its spherical shape. The animation is highly visual, responsive, and uses color gradients to give depth and vibrancy to the particles.



1<!DOCTYPE html> 2<html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>Morphing Particle Sphere - Interactive 3D Text Animation</title> 7 <link rel="stylesheet" href="./style.css"> 8 9 </head> 10 11 <body> 12 <link rel="preconnect" href="https://fonts.googleapis.com"> 13 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 14 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> 15 <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> 16 <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.1/gsap.min.js"></script> 17 <div id="container"></div> 18 19 <div class="input-container"> 20 <div class="input-wrapper"> 21 <input type="text" id="morphText" placeholder="Type something..." maxlength="20"> 22 <button id="typeBtn"> 23 <span class="button-content"> 24 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 25 <path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> 26 </svg> 27 <span>Create</span> 28 </span> 29 </button> 30 </div> 31 </div> 32 <script src="./script.js"></script> 33 34 </body> 35 36</html> 37

Explanation:

3D Particle Sphere: 12,000 particles form a sphere.

Text Morphing: User input text temporarily reshapes the particle sphere into letters.

Color Gradients: Particles use HSL color gradients based on depth for a glowing effect.

Floating & Rotation: Sphere slowly rotates for dynamic visual appeal.

Responsive Input: Users can type and click create or press enter to trigger morphing.

Animation Library: Uses GSAP for smooth transitions of positions and colors.


CSS Code


1* { 2 margin: 0; 3 padding: 0; 4 box-sizing: border-box; 5} 6 7body { 8 background: #000; 9 overflow: hidden; 10 font-family: 'Inter', sans-serif; 11 color: white; 12} 13 14#container { 15 position: fixed; 16 top: 0; 17 left: 0; 18 width: 100%; 19 height: 100%; 20} 21 22.header { 23 position: fixed; 24 top: 2rem; 25 left: 2rem; 26 text-align: left; 27 z-index: 1; 28 mix-blend-mode: difference; 29} 30 31.header h1 { 32 font-size: 2.5rem; 33 font-weight: 900; 34 line-height: 1; 35 text-transform: uppercase; 36 background: linear-gradient(45deg, #ff6e7f, #bfe9ff); 37 -webkit-background-clip: text; 38 background-clip: text; 39 color: transparent; 40 opacity: 0.9; 41 letter-spacing: -1px; 42 filter: drop-shadow(0 0 15px rgba(255,255,255,0.3)); 43} 44 45.color-controls { 46 position: fixed; 47 top: 2rem; 48 right: 2rem; 49 z-index: 10; 50 background: rgba(0, 0, 0, 0.3); 51 padding: 1rem; 52 border-radius: 15px; 53 backdrop-filter: blur(10px); 54 box-shadow: 0 0 20px rgba(0,0,0,0.3); 55} 56 57.color-scheme { 58 display: flex; 59 flex-direction: column; 60 gap: 0.5rem; 61} 62 63.color-scheme button { 64 display: flex; 65 align-items: center; 66 gap: 0.5rem; 67 background: rgba(255, 255, 255, 0.1); 68 border: 1px solid rgba(255, 255, 255, 0.2); 69 padding: 0.5rem 1rem; 70 color: white; 71 border-radius: 8px; 72 cursor: pointer; 73 transition: all 0.3s ease; 74 font-size: 0.9rem; 75 min-width: 120px; 76} 77 78.color-scheme button:hover { 79 background: rgba(255, 255, 255, 0.2); 80} 81 82.color-scheme button.active { 83 background: rgba(255, 255, 255, 0.3); 84 border-color: rgba(255, 255, 255, 0.5); 85} 86 87.color-preview { 88 width: 20px; 89 height: 20px; 90 border-radius: 50%; 91 border: 2px solid rgba(255, 255, 255, 0.3); 92} 93 94.color-preview.cosmic { 95 background: linear-gradient(45deg, #ff6e7f, #bfe9ff); 96} 97 98.color-preview.neon { 99 background: linear-gradient(45deg, #00ff87, #60efff); 100} 101 102.color-preview.sunset { 103 background: linear-gradient(45deg, #ff8c37, #ff427a); 104} 105 106.color-preview.ocean { 107 background: linear-gradient(45deg, #0082c8, #00b4db); 108} 109 110.controls { 111 position: fixed; 112 bottom: 2rem; 113 left: 50%; 114 transform: translateX(-50%); 115 display: flex; 116 gap: 1rem; 117 z-index: 10; 118 background: rgba(0, 0, 0, 0.3); 119 padding: 1rem; 120 border-radius: 50px; 121 backdrop-filter: blur(10px); 122 box-shadow: 0 0 20px rgba(0,0,0,0.3); 123} 124 125.controls button { 126 background: rgba(255, 255, 255, 0.1); 127 border: 1px solid rgba(255, 255, 255, 0.2); 128 padding: 0.8rem 1.5rem; 129 color: white; 130 border-radius: 25px; 131 cursor: pointer; 132 transition: all 0.3s ease; 133 text-transform: uppercase; 134 letter-spacing: 1px; 135 font-size: 0.9rem; 136 font-weight: 500; 137 min-width: 120px; 138} 139 140.controls button:hover { 141 background: rgba(255, 255, 255, 0.2); 142 transform: translateY(-2px); 143 box-shadow: 0 5px 15px rgba(0,0,0,0.3); 144} 145 146.controls button.active { 147 background: linear-gradient(45deg, #ff6e7f, #bfe9ff); 148 border: none; 149 color: #000; 150} 151 152@keyframes float { 153 0% { 154 transform: translate(-50%, -50%); 155 } 156 50% { 157 transform: translate(-50%, -52%); 158 } 159 100% { 160 transform: translate(-50%, -50%); 161 } 162} 163 164.content { 165 animation: float 4s ease-in-out infinite; 166} 167 168.input-container { 169 position: fixed; 170 bottom: 2rem; 171 left: 50%; 172 transform: translateX(-50%); 173 z-index: 10; 174 width: 90%; 175 max-width: 600px; 176 padding: 0 1rem; 177} 178 179.input-wrapper { 180 background: rgba(255, 255, 255, 0.1); 181 backdrop-filter: blur(10px); 182 border: 1px solid rgba(255, 255, 255, 0.2); 183 border-radius: 16px; 184 padding: 0.5rem; 185 display: flex; 186 gap: 0.5rem; 187 box-shadow: 0 4px 24px -1px rgba(0, 0, 0, 0.2); 188 transition: all 0.3s ease; 189} 190 191.input-wrapper:hover { 192 background: rgba(255, 255, 255, 0.15); 193 border-color: rgba(255, 255, 255, 0.3); 194 box-shadow: 0 4px 30px -1px rgba(0, 0, 0, 0.3); 195} 196 197input { 198 flex: 1; 199 background: transparent; 200 border: none; 201 padding: 1rem 1.25rem; 202 color: white; 203 font-size: 1rem; 204 font-weight: 500; 205} 206 207input:focus { 208 outline: none; 209} 210 211input::placeholder { 212 color: rgba(255, 255, 255, 0.5); 213} 214 215button { 216 background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); 217 border: none; 218 padding: 0.75rem 1.5rem; 219 color: white; 220 border-radius: 12px; 221 font-weight: 600; 222 cursor: pointer; 223 transition: all 0.3s ease; 224 display: flex; 225 align-items: center; 226 gap: 0.5rem; 227} 228 229button:hover { 230 transform: translateY(-1px); 231 box-shadow: 0 4px 20px -2px rgba(79, 70, 229, 0.5); 232} 233 234button:active { 235 transform: translateY(1px); 236} 237 238.button-content { 239 display: flex; 240 align-items: center; 241 gap: 0.5rem; 242} 243 244.button-content svg { 245 width: 20px; 246 height: 20px; 247 transition: transform 0.3s ease; 248} 249 250button:hover .button-content svg { 251 transform: translateX(3px); 252} 253 254@media (max-width: 640px) { 255 .input-container { 256 bottom: 1.5rem; 257 padding: 0 0.75rem; 258 } 259 260 button { 261 padding: 0.75rem 1.25rem; 262 } 263 264 .button-content span { 265 display: none; 266 } 267}

Explanation


Body & Container:

Black background and overflow hidden.

#container fills full viewport and holds Three.js canvas.

Input Wrapper:

Semi-transparent blurred background with rounded edges.

Flex layout for input field and button.

Hover effects enhance interactivity.

Button Styles:

Gradient background with hover and active states.

SVG icon animates slightly on hover.

Floating Animation:

@keyframes float gently moves the UI elements to create motion.

Color Controls & UI:

Optional color scheme buttons to change particle colors dynamically.

Buttons highlight active selection with increased opacity.


Javascript Code


1let scene, camera, renderer, particles; 2const count = 12000; 3let currentState = 'sphere'; 4 5function init() { 6 scene = new THREE.Scene(); 7 camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 8 renderer = new THREE.WebGLRenderer({ antialias: true }); 9 renderer.setSize(window.innerWidth, window.innerHeight); 10 renderer.setClearColor(0x000000); 11 document.getElementById('container').appendChild(renderer.domElement); 12 13 camera.position.z = 25; 14 15 createParticles(); 16 setupEventListeners(); 17 animate(); 18} 19 20function createParticles() { 21 const geometry = new THREE.BufferGeometry(); 22 const positions = new Float32Array(count * 3); 23 const colors = new Float32Array(count * 3); 24 25 function sphericalDistribution(i) { 26 const phi = Math.acos(-1 + (2 * i) / count); 27 const theta = Math.sqrt(count * Math.PI) * phi; 28 29 return { 30 x: 8 * Math.cos(theta) * Math.sin(phi), 31 y: 8 * Math.sin(theta) * Math.sin(phi), 32 z: 8 * Math.cos(phi) 33 }; 34 } 35 36 for (let i = 0; i < count; i++) { 37 const point = sphericalDistribution(i); 38 39 positions[i * 3] = point.x + (Math.random() - 0.5) * 0.5; 40 positions[i * 3 + 1] = point.y + (Math.random() - 0.5) * 0.5; 41 positions[i * 3 + 2] = point.z + (Math.random() - 0.5) * 0.5; 42 43 const color = new THREE.Color(); 44 const depth = Math.sqrt(point.x * point.x + point.y * point.y + point.z * point.z) / 8; 45 color.setHSL(0.5 + depth * 0.2, 0.7, 0.4 + depth * 0.3); 46 47 colors[i * 3] = color.r; 48 colors[i * 3 + 1] = color.g; 49 colors[i * 3 + 2] = color.b; 50 } 51 52 geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 53 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 54 55 const material = new THREE.PointsMaterial({ 56 size: 0.08, 57 vertexColors: true, 58 blending: THREE.AdditiveBlending, 59 transparent: true, 60 opacity: 0.8, 61 sizeAttenuation: true 62 }); 63 64 if (particles) scene.remove(particles); 65 particles = new THREE.Points(geometry, material); 66 particles.rotation.x = 0; 67 particles.rotation.y = 0; 68 particles.rotation.z = 0; 69 scene.add(particles); 70} 71 72function setupEventListeners() { 73 const typeBtn = document.getElementById('typeBtn'); 74 const input = document.getElementById('morphText'); 75 76 typeBtn.addEventListener('click', () => { 77 const text = input.value.trim(); 78 if (text) { 79 morphToText(text); 80 } 81 }); 82 83 input.addEventListener('keypress', (e) => { 84 if (e.key === 'Enter') { 85 const text = input.value.trim(); 86 if (text) { 87 morphToText(text); 88 } 89 } 90 }); 91} 92 93function createTextPoints(text) { 94 const canvas = document.createElement('canvas'); 95 const ctx = canvas.getContext('2d'); 96 const fontSize = 100; 97 const padding = 20; 98 99 ctx.font = `bold ${fontSize}px Arial`; 100 const textMetrics = ctx.measureText(text); 101 const textWidth = textMetrics.width; 102 const textHeight = fontSize; 103 104 canvas.width = textWidth + padding * 2; 105 canvas.height = textHeight + padding * 2; 106 107 ctx.fillStyle = 'white'; 108 ctx.font = `bold ${fontSize}px Arial`; 109 ctx.textBaseline = 'middle'; 110 ctx.textAlign = 'center'; 111 ctx.fillText(text, canvas.width / 2, canvas.height / 2); 112 113 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 114 const pixels = imageData.data; 115 const points = []; 116 const threshold = 128; 117 118 for (let i = 0; i < pixels.length; i += 4) { 119 if (pixels[i] > threshold) { 120 const x = (i / 4) % canvas.width; 121 const y = Math.floor((i / 4) / canvas.width); 122 123 if (Math.random() < 0.3) { 124 points.push({ 125 x: (x - canvas.width / 2) / (fontSize / 10), 126 y: -(y - canvas.height / 2) / (fontSize / 10) 127 }); 128 } 129 } 130 } 131 132 return points; 133} 134 135function morphToText(text) { 136 currentState = 'text'; 137 const textPoints = createTextPoints(text); 138 const positions = particles.geometry.attributes.position.array; 139 const targetPositions = new Float32Array(count * 3); 140 141 gsap.to(particles.rotation, { 142 x: 0, 143 y: 0, 144 z: 0, 145 duration: 0.5 146 }); 147 148 for (let i = 0; i < count; i++) { 149 if (i < textPoints.length) { 150 targetPositions[i * 3] = textPoints[i].x; 151 targetPositions[i * 3 + 1] = textPoints[i].y; 152 targetPositions[i * 3 + 2] = 0; 153 } else { 154 const angle = Math.random() * Math.PI * 2; 155 const radius = Math.random() * 20 + 10; 156 targetPositions[i * 3] = Math.cos(angle) * radius; 157 targetPositions[i * 3 + 1] = Math.sin(angle) * radius; 158 targetPositions[i * 3 + 2] = (Math.random() - 0.5) * 10; 159 } 160 } 161 162 for (let i = 0; i < positions.length; i += 3) { 163 gsap.to(particles.geometry.attributes.position.array, { 164 [i]: targetPositions[i], 165 [i + 1]: targetPositions[i + 1], 166 [i + 2]: targetPositions[i + 2], 167 duration: 2, 168 ease: "power2.inOut", 169 onUpdate: () => { 170 particles.geometry.attributes.position.needsUpdate = true; 171 } 172 }); 173 } 174 175 setTimeout(() => { 176 morphToCircle(); 177 }, 4000); 178} 179 180function morphToCircle() { 181 currentState = 'sphere'; 182 const positions = particles.geometry.attributes.position.array; 183 const targetPositions = new Float32Array(count * 3); 184 const colors = particles.geometry.attributes.color.array; 185 186 function sphericalDistribution(i) { 187 const phi = Math.acos(-1 + (2 * i) / count); 188 const theta = Math.sqrt(count * Math.PI) * phi; 189 190 return { 191 x: 8 * Math.cos(theta) * Math.sin(phi), 192 y: 8 * Math.sin(theta) * Math.sin(phi), 193 z: 8 * Math.cos(phi) 194 }; 195 } 196 197 for (let i = 0; i < count; i++) { 198 const point = sphericalDistribution(i); 199 200 targetPositions[i * 3] = point.x + (Math.random() - 0.5) * 0.5; 201 targetPositions[i * 3 + 1] = point.y + (Math.random() - 0.5) * 0.5; 202 targetPositions[i * 3 + 2] = point.z + (Math.random() - 0.5) * 0.5; 203 204 const depth = Math.sqrt(point.x * point.x + point.y * point.y + point.z * point.z) / 8; 205 const color = new THREE.Color(); 206 color.setHSL(0.5 + depth * 0.2, 0.7, 0.4 + depth * 0.3); 207 208 colors[i * 3] = color.r; 209 colors[i * 3 + 1] = color.g; 210 colors[i * 3 + 2] = color.b; 211 } 212 213 for (let i = 0; i < positions.length; i += 3) { 214 gsap.to(particles.geometry.attributes.position.array, { 215 [i]: targetPositions[i], 216 [i + 1]: targetPositions[i + 1], 217 [i + 2]: targetPositions[i + 2], 218 duration: 2, 219 ease: "power2.inOut", 220 onUpdate: () => { 221 particles.geometry.attributes.position.needsUpdate = true; 222 } 223 }); 224 } 225 226 for (let i = 0; i < colors.length; i += 3) { 227 gsap.to(particles.geometry.attributes.color.array, { 228 [i]: colors[i], 229 [i + 1]: colors[i + 1], 230 [i + 2]: colors[i + 2], 231 duration: 2, 232 ease: "power2.inOut", 233 onUpdate: () => { 234 particles.geometry.attributes.color.needsUpdate = true; 235 } 236 }); 237 } 238} 239 240function animate() { 241 requestAnimationFrame(animate); 242 243 if (currentState === 'sphere') { 244 particles.rotation.y += 0.002; 245 } 246 247 renderer.render(scene, camera); 248} 249 250window.addEventListener('resize', () => { 251 camera.aspect = window.innerWidth / window.innerHeight; 252 camera.updateProjectionMatrix(); 253 renderer.setSize(window.innerWidth, window.innerHeight); 254}); 255 256init(); 257

Initialization

Creates Three.js scene, camera, and renderer.

Appends canvas to container div.

Calls createParticles() to generate the initial particle sphere.

Calls setupEventListeners() for user input interactions.

Calls animate() to start the render loop.

Particle Creation (createParticles)

Generates a BufferGeometry with 12,000 points.

Positions are initially distributed in a spherical pattern using sphericalDistribution.

Each particle is assigned a color based on its depth for gradient effect.

Creates PointsMaterial with additive blending and transparency.

Adds particles to the scene.

Event Listeners (setupEventListeners)

Listens for clicks on the “Create” button or pressing Enter in the input field.

Calls morphToText with input text when triggered.

Text Morphing (morphToText)

Converts user input text into a set of points (createTextPoints) using a canvas 2D context.

Animates each particle from its current position to the corresponding text position using GSAP.

Extra particles not needed for text are scattered randomly.

After 4 seconds, automatically calls morphToCircle to return to spherical form.

Return to Sphere (morphToCircle)

Particles smoothly return to a spherical distribution.

Colors are recalculated for depth shading.

GSAP handles smooth transitions of positions and colors.

Animation Loop (animate)

Runs continuously with requestAnimationFrame.

Rotates the particle sphere slowly when in spherical state.

Calls renderer.render(scene, camera) on each frame.

Responsive Behavior

Updates camera aspect ratio and renderer size on window resize.

Love this component?

Explore more components and build amazing UIs.

View All Components