ParticlesYou can copy this
Black Hole
Particles spiral inward through a glowing gravitational vortex.
Motion is paused because your device asks for less motion.
Source code
<!-- Co-Creator preview. Owner-authored Sovereign Particles engine and preset configuration bundled; no third-party license grant. -->
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'"><style>html,body{margin:0;min-height:100%;background:#0b0b0c;color:#ededed;font:14px/1.4 system-ui,sans-serif}main{min-height:100vh;display:grid;grid-template-rows:minmax(220px,1fr) auto}#particle-stage{position:relative;min-height:320px;overflow:hidden}canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.info{display:flex;gap:16px;align-items:center;justify-content:space-between;padding:12px 16px;border-top:1px solid #29292b}.info p{margin:0;color:#bebec2}.info button{border:1px solid #69696f;border-radius:999px;background:#18181a;color:#fff;padding:7px 14px;font:inherit;cursor:pointer}@media (max-width:420px){.info{align-items:flex-start;flex-direction:column;gap:8px}}@media (prefers-reduced-motion:reduce){.info button{display:none}}@media print{.info{display:none}}</style><style>body.embed,body.embed main{height:100vh;min-height:0}body.embed main{grid-template-rows:1fr}body.embed #particle-stage{min-height:0;height:100%}body.embed .info{display:none}</style></head><body><main><div id="particle-stage"></div><section class="info"><p>Preview uses 1000 particles; increase <code>count</code> in config for production.</p><button id="toggle" type="button">Play</button></section></main><script>if(new URLSearchParams(location.search).get("embed")==="1")document.body.className="embed";</script><script>(() => {
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// ../../sovereign-particles/src/core/engine.ts
var ParticleEngine = class {
constructor(config2) {
__publicField(this, "particles");
__publicField(this, "config");
__publicField(this, "time");
this.config = config2;
this.time = 0;
this.particles = [];
this.init();
}
init() {
this.particles = [];
for (let i = 0; i < this.config.count; i++) {
this.particles.push(this.spawnParticle(i));
}
this.updateShapeTargets();
}
spawnParticle(index) {
const { spawnRadius, baseSize, sizeVariance, colors, maxLife, alphaRange, is3D, shape, shapeSnap } = this.config;
let x, y, z;
if (shape && shapeSnap) {
const pos = shape(index, this.config.count, 0);
x = pos[0];
y = pos[1];
z = pos[2];
} else {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = Math.random() * spawnRadius;
x = r * Math.sin(phi) * Math.cos(theta);
y = r * Math.sin(phi) * Math.sin(theta);
z = is3D ? r * Math.cos(phi) : 0;
}
const color = colors[Math.floor(Math.random() * colors.length)];
return {
x,
y,
z,
vx: (Math.random() - 0.5) * 0.02,
vy: (Math.random() - 0.5) * 0.02,
vz: is3D ? (Math.random() - 0.5) * 0.02 : 0,
tx: x,
ty: y,
tz: z,
r: color[0],
g: color[1],
b: color[2],
a: alphaRange[0] + Math.random() * (alphaRange[1] - alphaRange[0]),
size: baseSize + (Math.random() - 0.5) * sizeVariance,
life: Math.random() * maxLife,
maxLife,
index
};
}
/** Recalculate shape target positions for all particles */
updateShapeTargets() {
const { shape } = this.config;
if (!shape) return;
const count = this.particles.length;
for (let i = 0; i < count; i++) {
const pos = shape(i, count, this.time);
this.particles[i].tx = pos[0];
this.particles[i].ty = pos[1];
this.particles[i].tz = pos[2];
}
}
/** Set a new shape (triggers retargeting) */
setShape(shape) {
this.config.shape = shape;
if (shape) this.updateShapeTargets();
}
update(dt) {
this.time += dt;
const { forces, damping, speed, loop, shape, shapeAttract = 0.05, shapeNoise = 0 } = this.config;
if (shape) {
this.updateShapeTargets();
}
for (let i = 0; i < this.particles.length; i++) {
const p = this.particles[i];
if (shape && shapeAttract > 0) {
const dx = p.tx - p.x;
const dy = p.ty - p.y;
const dz = p.tz - p.z;
p.vx += dx * shapeAttract;
p.vy += dy * shapeAttract;
p.vz += dz * shapeAttract;
if (shapeNoise > 0) {
p.vx += Math.sin(p.index * 137.508 + this.time * 2) * shapeNoise * 0.01;
p.vy += Math.cos(p.index * 97.3 + this.time * 1.7) * shapeNoise * 0.01;
}
}
for (const force of forces) {
this.applyForce(p, force);
}
p.x += p.vx * speed * dt;
p.y += p.vy * speed * dt;
p.z += p.vz * speed * dt;
p.vx *= damping;
p.vy *= damping;
p.vz *= damping;
p.life -= dt;
const lifeRatio = Math.max(0, p.life / p.maxLife);
p.a = lifeRatio * this.config.alphaRange[1];
if (p.life <= 0 && loop) {
const fresh = this.spawnParticle(p.index);
Object.assign(p, fresh);
}
}
}
applyForce(p, force) {
const cx = force.center?.[0] ?? 0;
const cy = force.center?.[1] ?? 0;
const cz = force.center?.[2] ?? 0;
const dx = cx - p.x;
const dy = cy - p.y;
const dz = cz - p.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) + 1e-3;
const s = force.strength;
switch (force.type) {
case "attractor": {
const f = s / (dist * dist + 0.5);
p.vx += dx / dist * f;
p.vy += dy / dist * f;
p.vz += dz / dist * f;
break;
}
case "repulsor": {
const f = s / (dist * dist + 0.5);
p.vx -= dx / dist * f;
p.vy -= dy / dist * f;
p.vz -= dz / dist * f;
break;
}
case "vortex": {
const ax = force.axis?.[0] ?? 0;
const ay = force.axis?.[1] ?? 1;
const az = force.axis?.[2] ?? 0;
const f = s / (dist + 1);
p.vx += (dy * az - dz * ay) * f;
p.vy += (dz * ax - dx * az) * f;
p.vz += (dx * ay - dy * ax) * f;
break;
}
case "orbit": {
const f = s / (dist + 0.5);
p.vx += -dy / dist * f;
p.vy += dx / dist * f;
p.vx += dx / dist * f * 0.1;
p.vy += dy / dist * f * 0.1;
break;
}
case "noise": {
const freq = force.frequency ?? 1;
const sc = force.scale ?? 1;
p.vx += Math.sin(p.y * freq + this.time * 0.5) * s * sc * 0.01;
p.vy += Math.cos(p.x * freq + this.time * 0.7) * s * sc * 0.01;
p.vz += Math.sin((p.x + p.y) * freq + this.time * 0.3) * s * sc * 0.01;
break;
}
case "gravity": {
p.vy -= s * 1e-3;
break;
}
case "flow": {
const freq = force.frequency ?? 1;
p.vx += Math.sin(p.y * freq + this.time) * s * 5e-3;
p.vy += Math.cos(p.x * freq + this.time * 0.8) * s * 5e-3;
break;
}
}
}
/** Get flat position array for GPU buffers [x,y,z, x,y,z, ...] */
getPositions() {
const arr = new Float32Array(this.particles.length * 3);
for (let i = 0; i < this.particles.length; i++) {
arr[i * 3] = this.particles[i].x;
arr[i * 3 + 1] = this.particles[i].y;
arr[i * 3 + 2] = this.particles[i].z;
}
return arr;
}
/** Get flat color array [r,g,b,a, r,g,b,a, ...] */
getColors() {
const arr = new Float32Array(this.particles.length * 4);
for (let i = 0; i < this.particles.length; i++) {
arr[i * 4] = this.particles[i].r;
arr[i * 4 + 1] = this.particles[i].g;
arr[i * 4 + 2] = this.particles[i].b;
arr[i * 4 + 3] = this.particles[i].a;
}
return arr;
}
/** Get flat size array */
getSizes() {
const arr = new Float32Array(this.particles.length);
for (let i = 0; i < this.particles.length; i++) {
arr[i] = this.particles[i].size;
}
return arr;
}
/** Reset with new config */
reconfigure(config2) {
Object.assign(this.config, config2);
this.init();
}
};
// ../../sovereign-particles/src/core/presets.ts
var hex = (h) => [
parseInt(h.slice(1, 3), 16) / 255,
parseInt(h.slice(3, 5), 16) / 255,
parseInt(h.slice(5, 7), 16) / 255
];
var SOVEREIGN_SWARM = {
count: 8e3,
spawnRadius: 5,
baseSize: 0.03,
sizeVariance: 0.02,
speed: 1.2,
damping: 0.985,
colors: [hex("#FF5841"), hex("#FF7A68"), hex("#CC4633"), hex("#FFFFFF")],
forces: [
{ type: "orbit", strength: 0.15, center: [0, 0, 0] },
{ type: "attractor", strength: 0.02, center: [0, 0, 0] },
{ type: "noise", strength: 0.8, frequency: 2, scale: 1 }
],
loop: true,
maxLife: 8,
alphaRange: [0.1, 0.7],
is3D: true,
trailLength: 0
};
var NEURAL_WEB = {
count: 4e3,
spawnRadius: 6,
baseSize: 0.04,
sizeVariance: 0.03,
speed: 0.6,
damping: 0.992,
colors: [hex("#D5A078"), hex("#C5923A"), hex("#E8DCC8"), hex("#A0715A")],
forces: [
{ type: "flow", strength: 1.2, frequency: 0.8 },
{ type: "attractor", strength: 8e-3, center: [0, 0, 0] },
{ type: "noise", strength: 1.5, frequency: 1.5, scale: 1 }
],
loop: true,
maxLife: 12,
alphaRange: [0.05, 0.5],
is3D: true,
trailLength: 0
};
var BLACK_HOLE = {
count: 12e3,
spawnRadius: 8,
baseSize: 0.025,
sizeVariance: 0.015,
speed: 1.5,
damping: 0.978,
colors: [hex("#8B5CF6"), hex("#C4B5FD"), hex("#6D28D9"), hex("#FFFFFF")],
forces: [
{ type: "attractor", strength: 0.25, center: [0, 0, 0] },
{ type: "vortex", strength: 0.12, center: [0, 0, 0], axis: [0, 1, 0] },
{ type: "noise", strength: 0.3, frequency: 3, scale: 0.5 }
],
loop: true,
maxLife: 6,
alphaRange: [0.15, 0.85],
is3D: true,
trailLength: 0
};
var CYPHER_STREAM = {
count: 6e3,
spawnRadius: 8,
baseSize: 0.02,
sizeVariance: 0.015,
speed: 1,
damping: 0.995,
colors: [hex("#00FF41"), hex("#00CC34"), hex("#33FF66"), hex("#006622")],
forces: [
{ type: "gravity", strength: 3 },
{ type: "noise", strength: 0.5, frequency: 4, scale: 0.3 },
{ type: "flow", strength: 0.3, frequency: 2 }
],
loop: true,
maxLife: 5,
alphaRange: [0.1, 0.9],
is3D: false,
trailLength: 0
};
var COGNITIVE_FIELD = {
count: 3e3,
spawnRadius: 7,
baseSize: 0.05,
sizeVariance: 0.04,
speed: 0.3,
damping: 0.997,
colors: [hex("#D5FF5A"), hex("#A0CC44"), hex("#E8FFB0"), hex("#FFFFFF")],
forces: [
{ type: "flow", strength: 0.8, frequency: 0.5 },
{ type: "noise", strength: 2, frequency: 0.8, scale: 1.5 }
],
loop: true,
maxLife: 15,
alphaRange: [0.03, 0.35],
is3D: true,
trailLength: 0
};
var PRESETS = {
"sovereign-swarm": SOVEREIGN_SWARM,
"neural-web": NEURAL_WEB,
"black-hole": BLACK_HOLE,
"cypher-stream": CYPHER_STREAM,
"cognitive-field": COGNITIVE_FIELD
};
// <stdin>
var preset = "black-hole";
var previewCount = 1e3;
var host = document.getElementById("particle-stage");
var canvas = document.createElement("canvas");
canvas.setAttribute("aria-label", `${preset} particle preview`);
host.appendChild(canvas);
var ctx = canvas.getContext("2d");
var config = { ...PRESETS[preset], count: previewCount };
var engine = new ParticleEngine(config);
var width = 0;
var height = 0;
var raf = 0;
var playing = false;
var destroyed = false;
function resize() {
const ratio = Math.min(window.devicePixelRatio || 1, 2);
width = host.clientWidth;
height = host.clientHeight;
canvas.width = Math.max(1, Math.round(width * ratio));
canvas.height = Math.max(1, Math.round(height * ratio));
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
draw();
}
function draw() {
ctx.clearRect(0, 0, width, height);
ctx.globalCompositeOperation = "lighter";
const scale = Math.min(width, height) / (engine.config.spawnRadius * 2.5);
const cx = width / 2, cy = height / 2;
for (const p of engine.particles) {
if (p.a <= 0) continue;
const alpha = Math.max(0, Math.min(1, p.a));
const size = Math.max(0.5, p.size * scale * 0.5);
const r = Math.round(p.r * 255), g = Math.round(p.g * 255), b = Math.round(p.b * 255);
ctx.beginPath();
ctx.arc(cx + p.x * scale, cy + p.y * scale, size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
ctx.fill();
if (size > 1.5) {
ctx.beginPath();
ctx.arc(cx + p.x * scale, cy + p.y * scale, size * 2.5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${(alpha * 0.12).toFixed(3)})`;
ctx.fill();
}
}
ctx.globalCompositeOperation = "source-over";
}
function frame() {
if (!playing || destroyed) return;
engine.update(1 / 60);
draw();
raf = requestAnimationFrame(frame);
}
function setPlaying(next) {
if (destroyed || window.matchMedia("(prefers-reduced-motion: reduce)").matches) next = false;
if (playing === next) return;
playing = next;
if (playing) raf = requestAnimationFrame(frame);
else cancelAnimationFrame(raf);
const button = document.getElementById("toggle");
if (button) button.textContent = playing ? "Pause" : "Play";
if (window.parent !== window) window.parent.postMessage({ type: "design-preview", playing }, "*");
}
for (let i = 0; i < 12; i++) engine.update(1 / 60);
new ResizeObserver(resize).observe(host);
resize();
document.getElementById("toggle")?.addEventListener("click", () => setPlaying(!playing));
document.addEventListener("visibilitychange", () => {
if (document.hidden) setPlaying(false);
});
window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener("change", (event) => {
if (event.matches) setPlaying(false);
});
window.addEventListener("message", (event) => {
if (event.source !== window.parent || !event.data || event.data.type !== "design-preview" || typeof event.data.playing !== "boolean") return;
setPlaying(event.data.playing);
});
window.addEventListener("pagehide", () => {
destroyed = true;
cancelAnimationFrame(raf);
});
})();
</script></body></html>
Prompt
Use the black-hole preset in a self-contained visual for [PAGE_OR_COMPONENT]. Preserve existing content and functionality.
Dependencies
None — self-contained HTML
Use steps
- Copy the HTML or download the .html file.
- Save it as the listed filename and open it in a browser.
- Press Play to preview; edit the preset options to customize it.
- Test on mobile and with reduced motion enabled.