How do you turn a logo into particles?
- Sample. Draw the logo on an off-screen canvas, read its pixels and keep every Nth pixel that belongs to the logo. Each kept pixel is a target point.
- Assign. Create one particle per target point, with a random starting position.
- Move. On every frame, move each particle a fraction of the remaining distance to its target. The rule that decides the path is what makes one effect different from another.
- Draw. Clear one canvas and draw every particle as a small square or circle.
Step 1: How are the points sampled?
- The image is drawn on an off-screen canvas and read with
getImageData. If more than 5% of the pixels are transparent, the opaque part (alpha above 32) is the logo. Otherwise the sampler takes the colors of the four corners as the background and keeps the pixels that differ from it. The threshold and invert controls in the workbench tune that decision for light and dark backgrounds. - It checks a grid of pixels, where the spacing is the sampling step (2 to 12 in the workbench, 4 by default). Each point is nudged by less than half a pixel so the grid does not look rigid.
- Each point keeps its own RGB color. The workbench can also recolor the points with a palette.
- The points are sorted by their angle around the center, so effects that depend on the order of the points, such as pulses that travel around the outline, move smoothly.
- Detailed illustrations are sampled at a higher resolution than simple logos, so they get more points.
At the default step of 4, our three test logos gave 589 points (a white four-letter wordmark), 4,139 points (a five-color geometric mark) and 7,082 points (the Particle Logo mark). The workbench shows the count next to the preview. The code exports keep up to 1,600 of them, evenly spaced; the JSON export keeps all of them.
Step 2: Where do the particles start and where do they go?
Each particle gets a random start inside the canvas and one target point. On every frame the effect's code receives a progress value from 0 to 100, works out where the particle should be at that moment, and moves the particle part of the way there. The fraction is between 0.09 and 0.28 depending on the effect; higher is snappier.
Step 3: How does each effect move its particles?
The table follows the rules in src/engines/morphParticle.ts. An "offset" is extra displacement added on top of the target position.
| Effect | How the particles move |
|---|---|
| Quantum Condense | Each particle travels in a straight line from its start to its target at an even pace for the first 80% of the cycle, then overshoots slightly and settles. |
| Mobius Contour Flow | The same straight path, plus a sine wave that travels along the order of the points. It pushes particles in and out from the center by up to 4.5 px and fades as the logo forms. |
| Vortex Singularity | Each particle starts 2.25 turns around the center from its target and pushed outward by 65% of the longer canvas side. Both offsets shrink to zero as progress rises, so the particles wind inward. |
| Hologram Raster Scan | A scan line moves from the top of the canvas to the bottom. Particles whose target is above the line fly to it; the others wait near their start at low opacity. |
| Diffusion Denoise | Every particle aims for its target plus a random offset of up to ±24 px that shrinks to zero as progress reaches 100. |
| Superconducting Flux Wave | A ring expands from the center. Particles within 60 px of the ring are pushed up to 14 px along the direction from the center and drawn at full opacity; all particles also move toward their targets. |
| Autonomous Swarm Assembly | Particles are split into four groups by their index. Each group circles the center at 45% of the shorter canvas side; the circle shrinks by up to 80% as progress rises, and the particles ease out of their orbits into their targets. |
| Slingshot Time Reversal | For the first 38% of the cycle each particle sits 85 px farther from the center than its target. Then it snaps toward the target with a damped wobble of up to 14 px. |
| Cymatics Resonance | Every particle aims for its target plus a random offset whose size follows a two-dimensional standing-wave pattern (a sine times a cosine across the canvas) and shrinks to zero as progress rises. The offset is at most ±18 px. |
Step 4: How is it drawn?
- Every particle is drawn on one canvas with the 2D API:
fillRectfor squares andarcfor circles, in a singlerequestAnimationFrameloop. Each particle sets its fill color and opacity directly instead of callingsaveandrestoreone by one. - Each particle's size drifts between about 35% and 100%–165% of the base size, which gives the cloud a gentle shimmer.
- Particles within 70 px of the cursor are pushed away (85 px before the effect starts). A click gives nearby particles an impulse that loses 12% of its strength on every frame.
A canvas example you can run
This code uses only the first three steps and has no dependencies:
<canvas id="logo" width="480" height="480"></canvas>
<script>
const canvas = document.getElementById('logo');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = start;
img.src = 'logo.png'; // same origin; a PNG or SVG with a transparent background
function start() {
// 1. Draw the logo off-screen and read its pixels
const off = document.createElement('canvas');
off.width = canvas.width;
off.height = canvas.height;
const octx = off.getContext('2d', { willReadFrequently: true });
const scale = Math.min(off.width / img.width, off.height / img.height) * 0.8;
const w = img.width * scale, h = img.height * scale;
octx.drawImage(img, (off.width - w) / 2, (off.height - h) / 2, w, h);
const data = octx.getImageData(0, 0, off.width, off.height).data;
// 2. Keep every 4th pixel that is not transparent: these are the targets
const step = 4;
const particles = [];
for (let y = 0; y < off.height; y += step) {
for (let x = 0; x < off.width; x += step) {
const i = (y * off.width + x) * 4;
if (data[i + 3] > 128) {
particles.push({
x: Math.random() * canvas.width, // random start
y: Math.random() * canvas.height,
tx: x, ty: y, // target on the logo
color: 'rgb(' + data[i] + ',' + data[i + 1] + ',' + data[i + 2] + ')'
});
}
}
}
// 3. Every frame, move each particle a fraction of the way to its target
function frame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
p.x += (p.tx - p.x) * 0.06;
p.y += (p.ty - p.y) * 0.06;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - 1, p.y - 1, 2, 2);
}
requestAnimationFrame(frame);
}
frame();
}
</script>
- The example keeps pixels with alpha above 128, so the image must have a transparent background (a PNG or SVG). If the background is opaque, every pixel passes the check and you get a filled rectangle. The workbench compares pixels with the background color instead.
getImageDataonly works on same-origin images. If the image comes from another origin, or the page is opened fromfile://, the canvas is marked as tainted and reading it throws a security error. Serve the page and the image from the same server, or use a data URL.- Each frame leaves 94% of the remaining distance, so after about 75 frames (roughly 1.2 seconds at 60 fps) every particle is within 1% of its starting distance from the target.
- We ran this code on a 256 × 256 transparent PNG: it created 4,260 particles at a step of 4, and after 150 frames all of them sat on the logo's outline with the colors of the original.
Common questions
Does Particle Logo use WebGL or Three.js?
No. The exported files draw with the browser's 2D canvas API and have no runtime dependencies.
How many particles can an export hold?
Up to 1,600. The workbench can sample more: with the default step, our three test logos gave 589 to 7,082.
Why does my sampled logo have gaps?
Usually the sampling step is too large or the threshold is wrong. A smaller step captures more points; the threshold and invert switches change which pixels count as the logo, and the workbench's point cloud preview shows exactly which points were captured.
Do I need to write this myself?
No. The HTML, Vue 3 and React exports in the integration guides already include the nine effects, color settings, mouse interaction and a progress prop.