Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created May 24, 2025 13:16
Show Gist options
  • Save shricodev/cdb0493d71525d5988603b6c4dc86b2f to your computer and use it in GitHub Desktop.
Save shricodev/cdb0493d71525d5988603b6c4dc86b2f to your computer and use it in GitHub Desktop.
Blog - Morph Particles (Claude Opus 4)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GPU Particle Morphing - Next Level</title>
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
height: 100vh;
overflow: hidden;
background: #000;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#container {
width: 100%;
height: 100%;
position: relative;
}
#ui {
position: absolute;
top: 30px;
right: 30px;
display: flex;
flex-direction: column;
align-items: flex-end;
color: white;
z-index: 1000;
gap: 15px;
}
#controls {
position: absolute;
top: 30px;
left: 30px;
color: white;
z-index: 1000;
}
.control-group {
background: rgba(10, 10, 10, 0.8);
backdrop-filter: blur(20px);
padding: 20px;
border-radius: 20px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.slider-container {
margin: 15px 0;
}
.slider-container label {
display: block;
margin-bottom: 8px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.8);
}
.slider-container span {
color: #ff5900;
font-weight: 600;
}
input[type="range"] {
width: 220px;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.1);
outline: none;
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #ff5900;
cursor: pointer;
box-shadow: 0 0 10px rgba(255, 89, 0, 0.5);
transition: all 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 20px rgba(255, 89, 0, 0.8);
}
input[type="color"] {
width: 60px;
height: 35px;
border: none;
border-radius: 10px;
cursor: pointer;
background: transparent;
}
#title {
font-size: 36px;
font-weight: 800;
margin-bottom: 10px;
text-shadow: 0 0 20px rgba(255, 89, 0, 0.5);
background: linear-gradient(135deg, #ff5900, #ff9a00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 2px;
}
.button {
cursor: pointer;
padding: 15px 25px;
border: 2px solid transparent;
border-radius: 15px;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05),
rgba(255, 255, 255, 0)
);
backdrop-filter: blur(10px);
color: #fff;
font-size: 15px;
font-weight: 600;
letter-spacing: 1px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
min-width: 160px;
text-align: center;
position: relative;
overflow: hidden;
}
.button::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
transition: left 0.5s;
}
.button:hover {
background: linear-gradient(
135deg,
rgba(255, 89, 0, 0.2),
rgba(255, 89, 0, 0.1)
);
border-color: rgba(255, 89, 0, 0.5);
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(255, 89, 0, 0.3);
}
.button:hover::before {
left: 100%;
}
.button.active {
background: linear-gradient(
135deg,
rgba(255, 89, 0, 0.8),
rgba(255, 89, 0, 0.6)
);
border-color: #ff5900;
transform: translateY(-2px) scale(1.02);
box-shadow: 0 15px 40px rgba(255, 89, 0, 0.4);
}
#info {
position: absolute;
bottom: 30px;
left: 30px;
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.8);
pointer-events: none;
letter-spacing: 0.5px;
}
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: #ff5900;
z-index: 2000;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="ui">
<div id="title">PARTICLE MORPH</div>
<div class="button" data-shape="sphere">SPHERE</div>
<div class="button" data-shape="bird">BIRD</div>
<div class="button" data-shape="face">HUMAN</div>
<div class="button" data-shape="tree">TREE</div>
</div>
<div id="controls">
<div class="control-group">
<div class="slider-container">
<label>Particle Size: <span id="sizeValue">3.0</span></label>
<input
type="range"
id="particleSize"
min="0.5"
max="10"
step="0.1"
value="3.0"
/>
</div>
<div class="slider-container">
<label>Rotation Speed: <span id="speedValue">0.5</span></label>
<input
type="range"
id="rotationSpeed"
min="-2"
max="2"
step="0.1"
value="0.5"
/>
</div>
<div class="slider-container">
<label>Particle Color:</label>
<input type="color" id="particleColor" value="#ff5900" />
</div>
</div>
<div class="control-group">
<div class="slider-container">
<label>Bloom Intensity: <span id="bloomValue">1.5</span></label>
<input
type="range"
id="bloomStrength"
min="0"
max="3"
step="0.1"
value="1.5"
/>
</div>
<div class="slider-container">
<label>Motion Trail: <span id="trailValue">0.85</span></label>
<input
type="range"
id="motionTrail"
min="0"
max="0.98"
step="0.01"
value="0.85"
/>
</div>
</div>
</div>
<div id="info">
Click & drag to rotate | Scroll to zoom | Double-click to reset
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js";
// Global variables
let scene, camera, renderer, controls;
let particleSystem, particleMaterial;
let composer, bloomPass;
let clock = new THREE.Clock();
let selectedButton = null;
let previousRenderTarget, motionBlurComposer;
// Particle system parameters
const PARTICLE_COUNT = 20000;
let targetPositions = new Float32Array(PARTICLE_COUNT * 3);
let originalPositions = new Float32Array(PARTICLE_COUNT * 3);
let morphProgress = 1.0;
let isMorphing = false;
// Parameters
const params = {
particleSize: 3.0,
particleColor: new THREE.Color("#ff5900"),
rotationSpeed: 0.5,
bloomStrength: 1.5,
motionTrail: 0.85,
};
init();
animate();
function init() {
initScene();
initLights();
initControls();
createParticleSystem();
initPostProcessing();
initEventListeners();
// Set initial shape
const initialButton = document.querySelector('[data-shape="sphere"]');
initialButton.classList.add("active");
selectedButton = initialButton;
morphToShape("sphere");
}
function initScene() {
// Scene setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
scene.fog = new THREE.FogExp2(0x000000, 0.01);
// Camera setup
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
camera.position.set(40, 30, 60);
camera.lookAt(0, 0, 0);
// Renderer setup
renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance",
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.getElementById("container").appendChild(renderer.domElement);
// Motion blur render target
previousRenderTarget = new THREE.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
},
);
}
function initLights() {
const ambientLight = new THREE.AmbientLight(0x404040, 2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(20, 30, 20);
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0xff5900, 0.5, 100);
pointLight.position.set(0, 0, 0);
scene.add(pointLight);
}
function initControls() {
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 20;
controls.maxDistance = 150;
controls.autoRotate = false;
controls.target.set(0, 0, 0);
}
function createParticleSystem() {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(PARTICLE_COUNT * 3);
const colors = new Float32Array(PARTICLE_COUNT * 3);
const sizes = new Float32Array(PARTICLE_COUNT);
const randoms = new Float32Array(PARTICLE_COUNT * 4);
// Initialize with sphere positions
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i3 = i * 3;
// Random values for shader animation
randoms[i * 4] = Math.random();
randoms[i * 4 + 1] = Math.random();
randoms[i * 4 + 2] = Math.random();
randoms[i * 4 + 3] = Math.random();
// Initial color
const hue = 0.05 + Math.random() * 0.1;
const saturation = 0.8 + Math.random() * 0.2;
const lightness = 0.5 + Math.random() * 0.3;
const color = new THREE.Color().setHSL(hue, saturation, lightness);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
// Random sizes for variation
sizes[i] = 0.8 + Math.random() * 0.4;
}
geometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3),
);
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
geometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute("randoms", new THREE.BufferAttribute(randoms, 4));
// Shader material for GPU animation
particleMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uSize: { value: params.particleSize },
uColor: { value: params.particleColor },
uMorphProgress: { value: 1.0 },
uTargetPositions: { value: targetPositions },
},
vertexShader: `
uniform float uTime;
uniform float uSize;
uniform float uMorphProgress;
attribute float size;
attribute vec4 randoms;
attribute vec3 color;
varying vec3 vColor;
varying float vAlpha;
void main() {
// Base position with subtle animation
vec3 pos = position;
// Add subtle floating animation
pos += vec3(
sin(uTime * randoms.x + randoms.w * 6.28) * 0.1,
cos(uTime * randoms.y + randoms.w * 6.28) * 0.1,
sin(uTime * randoms.z + randoms.w * 6.28) * 0.1
) * (1.0 - uMorphProgress * 0.5);
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
// Size attenuation
float sizeAttenuation = 300.0 / -mvPosition.z;
gl_PointSize = uSize * size * sizeAttenuation;
// Color variation
vColor = color;
vAlpha = 0.8 + sin(uTime * randoms.x) * 0.2;
}
`,
fragmentShader: `
uniform vec3 uColor;
varying vec3 vColor;
varying float vAlpha;
void main() {
// Circular particle shape with soft edges
vec2 center = vec2(0.5);
float dist = distance(gl_PointCoord, center);
if (dist > 0.5) discard;
// Soft edge falloff
float strength = 1.0 - smoothstep(0.0, 0.5, dist);
strength = pow(strength, 2.0);
// Final color with glow effect
vec3 finalColor = mix(uColor, vColor, 0.5);
finalColor = pow(finalColor * 1.2, vec3(0.8)); // Brighten and adjust gamma
gl_FragColor = vec4(finalColor, strength * vAlpha);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
particleSystem = new THREE.Points(geometry, particleMaterial);
scene.add(particleSystem);
}
function initPostProcessing() {
// Main composer
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// Bloom pass for glow effect
bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
params.bloomStrength,
0.4,
0.85,
);
composer.addPass(bloomPass);
// Gamma correction
const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaCorrectionPass);
// Motion blur pass
const motionBlurShader = {
uniforms: {
tDiffuse: { value: null },
tPrevious: { value: previousRenderTarget.texture },
uBlendFactor: { value: params.motionTrail },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D tPrevious;
uniform float uBlendFactor;
varying vec2 vUv;
void main() {
vec4 current = texture2D(tDiffuse, vUv);
vec4 previous = texture2D(tPrevious, vUv);
gl_FragColor = mix(current, previous, uBlendFactor);
}
`,
};
const motionBlurPass = new ShaderPass(motionBlurShader);
composer.addPass(motionBlurPass);
}
function morphToShape(shape) {
const positions = particleSystem.geometry.attributes.position.array;
// Store current positions
for (let i = 0; i < positions.length; i++) {
originalPositions[i] = positions[i];
}
// Generate target positions based on shape
switch (shape) {
case "sphere":
generateSphere();
break;
case "bird":
generateBird();
break;
case "face":
generateFace();
break;
case "tree":
generateTree();
break;
}
morphProgress = 0;
isMorphing = true;
}
function generateSphere() {
const radius = 20;
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i3 = i * 3;
// Fibonacci sphere for even distribution
const y = 1 - (i / (PARTICLE_COUNT - 1)) * 2;
const radiusAtY = Math.sqrt(1 - y * y);
const theta = ((i + 1) % PARTICLE_COUNT) * 2.399963229728653;
targetPositions[i3] = radiusAtY * Math.cos(theta) * radius;
targetPositions[i3 + 1] = y * radius;
targetPositions[i3 + 2] = radiusAtY * Math.sin(theta) * radius;
}
}
function generateBird() {
const points = [];
const scale = 0.8;
// Wing structure
for (let u = 0; u <= 1; u += 0.02) {
for (let v = 0; v <= 1; v += 0.1) {
// Left wing
const wingSpan = 30 * (1 - u * u);
const x1 = -wingSpan * v * scale;
const y1 = Math.sin(v * Math.PI) * 5 * scale;
const z1 = u * 20 * scale - 10 * scale;
points.push(new THREE.Vector3(x1, y1, z1));
// Right wing
const x2 = wingSpan * v * scale;
points.push(new THREE.Vector3(x2, y1, z1));
}
}
// Body
for (let t = 0; t <= 1; t += 0.02) {
const bodyRadius = 3 * (1 - t * t * 0.5);
for (let theta = 0; theta < Math.PI * 2; theta += 0.2) {
const x = Math.cos(theta) * bodyRadius * scale;
const y = Math.sin(theta) * bodyRadius * scale;
const z = (t - 0.5) * 25 * scale;
points.push(new THREE.Vector3(x, y, z));
}
}
// Head
for (let i = 0; i < 200; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
const r = 4 * scale;
const x = r * Math.sin(phi) * Math.cos(theta);
const y = r * Math.sin(phi) * Math.sin(theta) + 3 * scale;
const z = r * Math.cos(phi) + 12 * scale;
points.push(new THREE.Vector3(x, y, z));
}
// Tail feathers
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI - Math.PI / 2;
for (let t = 0; t < 1; t += 0.05) {
const x = Math.cos(angle) * t * 15 * scale;
const y = Math.sin(angle) * t * 8 * scale;
const z = -10 * scale - t * 8 * scale;
points.push(new THREE.Vector3(x, y, z));
}
}
distributePointsToParticles(points);
}
function generateFace() {
const points = [];
const scale = 1.2;
// Head outline - ellipsoid
for (let theta = 0; theta <= Math.PI; theta += 0.05) {
for (let phi = 0; phi <= Math.PI * 2; phi += 0.1) {
const r = 15;
const x = r * Math.sin(theta) * Math.cos(phi) * scale;
const y = r * Math.cos(theta) * 1.2 * scale + 5;
const z = r * Math.sin(theta) * Math.sin(phi) * 0.8 * scale;
// Add some noise to make it more organic
const noise = (Math.random() - 0.5) * 1;
points.push(new THREE.Vector3(x + noise, y + noise, z + noise));
}
}
// Eyes
const eyePositions = [
[-6, 8, 6],
[6, 8, 6],
];
for (let eye of eyePositions) {
for (let i = 0; i < 150; i++) {
const angle = (i / 150) * Math.PI * 2;
const r = Math.random() * 2.5;
const x = eye[0] + Math.cos(angle) * r * scale;
const y = eye[1] + Math.sin(angle) * r * scale;
const z = eye[2] + Math.random() * scale;
points.push(new THREE.Vector3(x, y, z));
}
}
// Nose
for (let t = 0; t <= 1; t += 0.05) {
for (let i = 0; i < 50; i++) {
const x = (Math.random() - 0.5) * 3 * (1 - t) * scale;
const y = 5 - t * 7 * scale;
const z = 8 + Math.random() * 2 * scale;
points.push(new THREE.Vector3(x, y, z));
}
}
// Mouth
for (let t = 0; t <= 1; t += 0.02) {
const angle = t * Math.PI;
const r = 7;
const x = Math.cos(angle) * r * scale;
const y = -5 - Math.sin(angle) * 2 * scale;
const z = 6 + Math.random() * scale;
points.push(new THREE.Vector3(x, y, z));
// Add thickness
for (let i = 0; i < 10; i++) {
points.push(
new THREE.Vector3(
x + (Math.random() - 0.5) * 2,
y + (Math.random() - 0.5) * 2,
z + Math.random(),
),
);
}
}
// Cheeks - add volume
const cheekPositions = [
[-10, 0, 5],
[10, 0, 5],
];
for (let cheek of cheekPositions) {
for (let i = 0; i < 200; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
const r = 5 * Math.random();
const x = cheek[0] + r * Math.sin(phi) * Math.cos(theta) * scale;
const y = cheek[1] + r * Math.sin(phi) * Math.sin(theta) * scale;
const z = cheek[2] + r * Math.cos(phi) * scale;
points.push(new THREE.Vector3(x, y, z));
}
}
distributePointsToParticles(points);
}
function generateTree() {
const points = [];
const scale = 0.7;
// Trunk
const trunkHeight = 15;
const trunkRadius = 3;
for (let y = -15; y < -15 + trunkHeight; y += 0.5) {
const radiusAtHeight =
trunkRadius * (1 + ((y + 15) / trunkHeight) * 0.2);
for (let angle = 0; angle < Math.PI * 2; angle += 0.1) {
const x = Math.cos(angle) * radiusAtHeight * scale;
const z = Math.sin(angle) * radiusAtHeight * scale;
points.push(new THREE.Vector3(x, y * scale, z));
}
}
// Branches with recursive structure
function addBranch(origin, direction, length, radius, level) {
if (level > 3 || length < 1) return;
const segments = 10;
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const pos = origin
.clone()
.add(direction.clone().multiplyScalar(length * t));
// Add thickness
for (let j = 0; j < 5; j++) {
const offset = new THREE.Vector3(
(Math.random() - 0.5) * radius,
(Math.random() - 0.5) * radius,
(Math.random() - 0.5) * radius,
);
points.push(pos.clone().add(offset));
}
// Create sub-branches
if (i === segments) {
const branches = 2 + Math.floor(Math.random() * 2);
for (let b = 0; b < branches; b++) {
const newDir = direction.clone();
newDir.x += (Math.random() - 0.5) * 0.8;
newDir.y += Math.random() * 0.5;
newDir.z += (Math.random() - 0.5) * 0.8;
newDir.normalize();
addBranch(pos, newDir, length * 0.7, radius * 0.6, level + 1);
}
}
}
}
// Main branches from trunk
const numMainBranches = 6;
for (let i = 0; i < numMainBranches; i++) {
const angle = (i / numMainBranches) * Math.PI * 2;
const height = -15 + trunkHeight * (0.5 + Math.random() * 0.3);
const origin = new THREE.Vector3(
Math.cos(angle) * trunkRadius * scale,
height * scale,
Math.sin(angle) * trunkRadius * scale,
);
const direction = new THREE.Vector3(
Math.cos(angle) * 0.7,
0.6 + Math.random() * 0.3,
Math.sin(angle) * 0.7,
).normalize();
addBranch(origin, direction, 12 * scale, 2 * scale, 0);
}
// Foliage clusters
for (let cluster = 0; cluster < 30; cluster++) {
const theta = Math.random() * Math.PI;
const phi = Math.random() * Math.PI * 2;
const r = 15 + Math.random() * 10;
const cx = r * Math.sin(theta) * Math.cos(phi) * scale;
const cy = r * (0.5 + Math.random() * 0.5) * scale;
const cz = r * Math.sin(theta) * Math.sin(phi) * scale;
// Create leaf cluster
for (let leaf = 0; leaf < 50; leaf++) {
const leafR = Math.random() * 3;
const leafTheta = Math.random() * Math.PI;
const leafPhi = Math.random() * Math.PI * 2;
const x =
cx + leafR * Math.sin(leafTheta) * Math.cos(leafPhi) * scale;
const y = cy + leafR * Math.cos(leafTheta) * scale;
const z =
cz + leafR * Math.sin(leafTheta) * Math.sin(leafPhi) * scale;
points.push(new THREE.Vector3(x, y, z));
}
}
// Roots
const numRoots = 8;
for (let i = 0; i < numRoots; i++) {
const angle = (i / numRoots) * Math.PI * 2 + Math.random() * 0.5;
for (let t = 0; t < 1; t += 0.05) {
const spread = t * 10;
const x = Math.cos(angle) * spread * scale;
const y = -15 - t * 5 * scale;
const z = Math.sin(angle) * spread * scale;
for (let j = 0; j < 5; j++) {
points.push(
new THREE.Vector3(
x + (Math.random() - 0.5) * 2,
y + Math.random(),
z + (Math.random() - 0.5) * 2,
),
);
}
}
}
distributePointsToParticles(points);
}
function distributePointsToParticles(points) {
// Ensure we have enough points
while (points.length < PARTICLE_COUNT) {
points.push(
points[Math.floor(Math.random() * points.length)].clone(),
);
}
// Shuffle points for better distribution
for (let i = points.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[points[i], points[j]] = [points[j], points[i]];
}
// Assign to particles
for (let i = 0; i < PARTICLE_COUNT; i++) {
const point = points[i % points.length];
targetPositions[i * 3] = point.x;
targetPositions[i * 3 + 1] = point.y;
targetPositions[i * 3 + 2] = point.z;
}
}
function initEventListeners() {
// Shape buttons
document.querySelectorAll(".button").forEach((button) => {
button.addEventListener("click", (e) => {
if (selectedButton) {
selectedButton.classList.remove("active");
}
selectedButton = e.target;
selectedButton.classList.add("active");
const shape = e.target.dataset.shape;
morphToShape(shape);
});
});
// Controls
document
.getElementById("particleSize")
.addEventListener("input", (e) => {
params.particleSize = parseFloat(e.target.value);
particleMaterial.uniforms.uSize.value = params.particleSize;
document.getElementById("sizeValue").textContent =
params.particleSize.toFixed(1);
});
document
.getElementById("rotationSpeed")
.addEventListener("input", (e) => {
params.rotationSpeed = parseFloat(e.target.value);
document.getElementById("speedValue").textContent =
params.rotationSpeed.toFixed(1);
});
document
.getElementById("particleColor")
.addEventListener("input", (e) => {
params.particleColor.set(e.target.value);
particleMaterial.uniforms.uColor.value = params.particleColor;
});
document
.getElementById("bloomStrength")
.addEventListener("input", (e) => {
params.bloomStrength = parseFloat(e.target.value);
bloomPass.strength = params.bloomStrength;
document.getElementById("bloomValue").textContent =
params.bloomStrength.toFixed(1);
});
document
.getElementById("motionTrail")
.addEventListener("input", (e) => {
params.motionTrail = parseFloat(e.target.value);
document.getElementById("trailValue").textContent =
params.motionTrail.toFixed(2);
});
// Window resize
window.addEventListener("resize", onWindowResize);
// Double-click reset
renderer.domElement.addEventListener("dblclick", () => {
controls.reset();
});
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
previousRenderTarget.setSize(window.innerWidth, window.innerHeight);
}
function updateMorphing(deltaTime) {
if (!isMorphing) return;
morphProgress += deltaTime * 0.8; // Morph speed
if (morphProgress >= 1.0) {
morphProgress = 1.0;
isMorphing = false;
}
// Smooth easing function
const eased = 1 - Math.pow(1 - morphProgress, 3);
// Update particle positions
const positions = particleSystem.geometry.attributes.position.array;
for (let i = 0; i < positions.length; i++) {
positions[i] =
originalPositions[i] +
(targetPositions[i] - originalPositions[i]) * eased;
}
particleSystem.geometry.attributes.position.needsUpdate = true;
particleMaterial.uniforms.uMorphProgress.value = eased;
}
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
// Update particle system
if (particleSystem) {
particleSystem.rotation.y += params.rotationSpeed * deltaTime;
particleMaterial.uniforms.uTime.value = elapsedTime;
updateMorphing(deltaTime);
}
// Store current frame for motion blur
renderer.setRenderTarget(previousRenderTarget);
renderer.render(scene, camera);
renderer.setRenderTarget(null);
// Update motion blur uniform
if (composer.passes[3]) {
composer.passes[3].uniforms.uBlendFactor.value = params.motionTrail;
}
// Render with post-processing
composer.render();
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment