Make a Spinning Particle Galaxy in Three.js (TypeScript)
Video: Make a Spinning Particle Galaxy in Three.js (TypeScript) by CelesteAI
A glowing spiral galaxy made of thousands of points looks like a lot of work. It isn’t. The whole thing is one loop that places each star, a material that makes overlapping points glow, and a render loop that slowly spins it. About fifty lines of Three.js.
This build uses TypeScript and Vite — scaffolded from the command line, written in the editor, and run in the browser.
Set up the project
Three.js is an ES module, so use a real bundler. Vite scaffolds a TypeScript project in one command:
npm create vite@latest particle-galaxy -- --template vanilla-ts
cd particle-galaxy
npm install
npm install three
Vite resolves the bare three import for you and the type definitions ship with the package — no extra @types needed. The starter gives you an index.html, a tsconfig.json, and a src/ folder with main.ts.
Trim index.html down to a full-screen canvas:
<canvas id="bg"></canvas>
<script type="module" src="/src/main.ts"></script>
Scene, camera, renderer
import * as THREE from "three";
const canvas = document.getElementById("bg") as HTMLCanvasElement;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setClearColor(0x05060a, 1);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 3.2, 6);
camera.lookAt(0, 0, 0);
The camera sits above and back from the origin, looking down at where the galaxy will sit.
The parameters
A galaxy is just a few numbers. Pull them out so they’re easy to tweak:
const COUNT = 14000; // stars
const BRANCHES = 3; // spiral arms
const RADIUS = 5;
const SPIN = 1.1; // how hard the arms twist
const RANDOMNESS = 0.45; // scatter around each arm
const RAND_POWER = 3; // bias scatter toward the arm
const INSIDE = new THREE.Color(0xff6030); // warm core
const OUTSIDE = new THREE.Color(0x1b3984); // cool edge
The loop that makes the galaxy
This is the only interesting part. For each star: pick a random radius, drop it onto one of the branches, and rotate it by a spin angle that grows with the radius — that twist is what turns straight arms into a spiral. A little random scatter (biased toward the arm with Math.pow) gives the arms thickness, and the color lerps from the warm center to the cool rim.
const positions = new Float32Array(COUNT * 3);
const colors = new Float32Array(COUNT * 3);
for (let i = 0; i < COUNT; i++) {
const i3 = i * 3;
const r = Math.random() * RADIUS;
const branchAngle = ((i % BRANCHES) / BRANCHES) * Math.PI * 2;
const spinAngle = r * SPIN;
const scatter = () => Math.pow(Math.random(), RAND_POWER) * (Math.random() < 0.5 ? 1 : -1) * RANDOMNESS * r;
positions[i3] = Math.cos(branchAngle + spinAngle) * r + scatter();
positions[i3 + 1] = scatter() * 0.5;
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * r + scatter();
const mixed = INSIDE.clone().lerp(OUTSIDE, r / RADIUS);
colors[i3] = mixed.r; colors[i3 + 1] = mixed.g; colors[i3 + 2] = mixed.b;
}
Geometry, material, and the glow
Hand the flat arrays to a BufferGeometry as attributes, then use a PointsMaterial with additive blending — where stars overlap, their colors add together and bloom into bright white, exactly like a real galaxy core. vertexColors tells the material to use the per-star color attribute.
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.025,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
});
const galaxy = new THREE.Points(geometry, material);
scene.add(galaxy);
Spin it
A clock-driven render loop rotates the whole Points object on its Y axis:
const clock = new THREE.Clock();
function tick() {
galaxy.rotation.y = clock.getElapsedTime() * 0.18;
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
tick();
Run it
npm run dev
Vite prints a local URL — open it and the galaxy turns. Change BRANCHES to 5, swap the two colors, or push SPIN higher, and you get a completely different galaxy from the same loop.
Recap
- A particle system is just flat arrays of positions and colors handed to a
BufferGeometry— no per-star objects. - The spiral is one idea: twist each star further the farther out it sits (
spinAngle = r * SPIN). - Additive blending is what makes overlapping points glow like a galaxy core.
Want the deeper dive?
The full source is on GitHub — clone it, npm install, npm run dev, and start tweaking the parameters.