# Stellar Flow – Interactive Particle Scene | Particleify

> Create flowing particle spirals, galaxies, glowing typography, and image effects, then export standalone HTML or React code with Particleify Pro.

[PParticleify](/)

GeneratorType LabNebulaStellar FlowParticle Attractor

[All scenes](/scenes.md)

Controls

Controls

# Stellar Flow

LIVE PREVIEW01 / STELLAR

Export code · ProMP4 · Free

12,000 PARTICLES/— FPS

Move your pointer to touch the stars

## Controls

Reset✕

01 — Starting shape

✧ Stellar spiral◎ Spiral galaxyAa Text particles▧ Image particles

Two arms · Inward flow

Inward flow1.0×Core light1.0

02 — Motion & particles

Motion speed1.0×Particle size1.3×Particle count12,000Shape scale1.0×Flow amplitude0.16

03 — Light & color

Brilliance1.3×Bright star density7Twinkle0.7Glow0.8Starburst1.0Ray length1.0×

Primary#a9dbffAccent#ffa66bBackground#03090e

More settings

Spatial depth0.5Pointer influence0.7Rotation0°

Settings save automatically in this browser.

## Export code

HTMLReact

Standalone interactive HTML with the current Stellar Flow settings. It contains the complete renderer and can be hosted as a single file.

```
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Particleify Stellar Flow</title>
  <style>
    * { box-sizing: border-box; }
    html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
    body { background: #03090e; }
    canvas { display: block; width: 100%; height: 100%; touch-action: none; }
    #error {
      position: fixed; left: 50%; bottom: 24px; z-index: 10; display: none;
      width: min(440px, calc(100% - 32px)); transform: translateX(-50%);
      border: 1px solid rgba(252, 211, 77, .24); background: rgba(36, 32, 25, .96);
      padding: 14px 18px; color: #fef3c7; font: 13px/1.6 ui-monospace, monospace;
    }
  </style>
</head>
<body>
  <canvas id="particle-canvas" role="img" aria-label="Flowing particle effect"></canvas>
  <div id="error" role="alert"></div>
  <script>

const CONFIG = {"mode":"six","text":"HELLO","image":"","speed":1,"size":1.3,"count":12000,"scale":1,"scatter":0.16,"glow":0.8,"depth":0.5,"interaction":0.65,"brilliance":1.3,"starDensity":7,"twinkle":0.7,"flowSpeed":1,"starRays":1,"rayLength":1,"coreGlow":1,"rotation":0,"color":"#a9dbff","accentColor":"#ffa66b","background":"#03090e","imageColors":true,"invert":false,"threshold":90,"paused":false};
const PARTICLE_VERTEX_SHADER = "\nprecision highp float;\nattribute vec3 aFrom;\nattribute vec3 aTarget;\nattribute vec3 aColor;\nattribute vec3 aSeed;\nattribute vec3 aActivity;\nattribute vec3 aRayMask;\nuniform vec2 uResolution;\nuniform vec2 uPointer;\nuniform float uTime, uMorph, uSize, uScale, uScatter, uDepth, uInteraction, uRotation, uDpr, uIntro, uMaxPoint;\nuniform float uBrilliance, uStarDensity, uTwinkle, uFootprint;\nuniform float uDensityScale;\nuniform mediump float uStarRays, uRayLength;\nuniform float uCoreGlow, uOrbit;\nvarying vec3 vColor;\nvarying float vBrightness;\nvarying float vStar;\nvarying float vExtent;\nvoid main(){\n  float m=smoothstep(0.,1.,uMorph);\n  vec3 p=mix(aFrom,aTarget,m);\n  float phase=aSeed.x*62.83;\n  float time=uTime*(.32+aSeed.y*.35);\n  vec3 motion=vec3(sin(time+phase),cos(time*.81+phase*1.3),sin(time*.6+phase*2.));\n  p+=motion*(.001+uScatter*mix(.035,.006,uOrbit));\n  p+=motion*sin(m*3.14159)*.13;\n  p=mix(vec3(sin(phase)*aSeed.y*1.3,cos(phase)*aSeed.z, sin(phase*2.)*.7),p,smoothstep(0.,1.,uIntro));\n  float c=cos(uRotation),s=sin(uRotation);\n  p.xy=mat2(c,s,-s,c)*p.xy*uScale;\n  vec2 delta=p.xy-uPointer;\n  float dist=length(delta);\n  p.xy+=delta/max(dist,.001)*exp(-dist*dist/0.011)*uInteraction*.05;\n  p.xy+=uPointer*p.z*uDepth*.3*(1.-step(5.,uPointer.x));\n  p.z*=uDepth;\n  float perspective=1./(1.+p.z*.7);\n  vec2 clip=p.xy*perspective*2.;\n  clip.x/=uResolution.x/uResolution.y;\n  gl_Position=vec4(clip.x,-clip.y,0.,1.);\n  float bright=step(1.-uStarDensity*.01*uDensityScale,aSeed.z);\n  float beacon=step(1.-uStarDensity*.0025*uDensityScale,aSeed.z)*bright;\n  float wave=.5+.5*sin(uTime*(.6+aSeed.y*.8)+phase);\n  float flare=pow(wave,7.)*uTwinkle;\n  float shimmer=1.-uTwinkle*.3+uTwinkle*.3*sin(uTime*(.85+aSeed.y)+phase*1.7);\n  float diameter=mix(2.8+aSeed.y*2.5,21.+aSeed.y*13.,bright);\n  diameter=mix(diameter,42.+aSeed.y*24.,beacon);\n  diameter*=mix(1.,aActivity.y,uOrbit*.6);\n  float crossStar=beacon*aRayMask.x;\n  vExtent=1.+crossStar*uRayLength*.8*step(.01,uStarRays);\n  float screenScale=clamp(min(uResolution.x,uResolution.y)/850.,.38,1.);\n  gl_PointSize=min(uMaxPoint,diameter*(1.+flare*.14*bright)*uSize*uDpr*perspective*screenScale*uFootprint*vExtent);\n  if(aSeed.z>1.5){gl_PointSize=min(uMaxPoint,280.*uDpr*uSize*screenScale);vExtent=1.;}\n  vStar=bright+beacon+step(1.5,aSeed.z);\n  vBrightness=mix(.35+aSeed.y*.65,.65+aSeed.y*.45,bright)*shimmer;\n  vBrightness*=mix(1.,uBrilliance,bright)*(1.+flare*.7*bright);\n  vBrightness*=mix(1.,aActivity.x,uOrbit);\n  vBrightness*=1.+uCoreGlow*aActivity.z*.18*uOrbit;\n  if(aSeed.z>1.5)vBrightness=uCoreGlow*(.6+.4*smoothstep(.25,1.,uIntro))*(.94+.06*sin(uTime*.65));\n  vColor=aColor;\n  if(length(aColor)\u003c.01) vColor=vec3(-1.,aSeed.x,bright);\n}";
const PARTICLE_FRAGMENT_SHADER = "\nprecision mediump float;\nuniform vec3 uColor, uAccent;\nuniform float uGlow;\nuniform float uStarRays, uRayLength;\nvarying vec3 vColor;\nvarying float vBrightness;\nvarying float vStar;\nvarying float vExtent;\nvoid main(){\n  vec2 q=(gl_PointCoord-.5)*2.*vExtent;\n  float r=length(q);\n  if(r>vExtent)discard;\n  float bright=step(.5,vStar),beacon=step(1.5,vStar);\n  float core=mix(exp(-r*r*18.),1.-smoothstep(.025,.12,r),bright);\n  float corona=exp(-r*r*95.)*.4*bright;\n  float halo=(exp(-r*r*9.)*.21+exp(-r*r*3.)*.06)*uGlow*bright;\n  vec2 ray=q/(max(uRayLength,.3)*.7);\n  float horizontal=exp(-abs(q.y)*95.)*(1.-smoothstep(.08,1.35,abs(ray.x)));\n  float vertical=exp(-abs(q.x)*95.)*(1.-smoothstep(.08,1.35,abs(ray.y)));\n  float rays=max(horizontal,vertical)*.4*uStarRays*beacon*step(1.001,vExtent);\n  vec3 tint=vColor.x\u003c0.?mix(uColor,uAccent,step(.77,vColor.y)):vColor;\n  vec3 coreColor=vColor.x\u003c0.?mix(tint,vec3(1.),bright*.83):tint;\n  vec3 energy=coreColor*core+tint*(corona+halo+rays);\n  if(vStar>2.5)energy=mix(tint,vec3(1.),.75)*exp(-r*r*5.)*.5*uGlow;\n  // Premultiply radiance: additive blending preserves HDR-like overlapping\n  // halos without clipping every individual star to a flat white square.\n  gl_FragColor=vec4(energy*vBrightness,1.);\n}";
const BLOOM_VERTEX_SHADER = "attribute vec2 aPosition;varying vec2 vUv;void main(){vUv=(aPosition+1.)*.5;gl_Position=vec4(aPosition,0.,1.);}";
const BLOOM_FRAGMENT_SHADER = "precision mediump float;\nvarying vec2 vUv;uniform sampler2D uSource;uniform sampler2D uHalo;\nuniform vec2 uStep;uniform vec3 uBackground;uniform float uPass,uStrength;\nvec3 readLight(vec2 uv){return texture2D(uSource,uv).rgb;}\nvoid main(){\n if(uPass>2.5){vec3 c=texture2D(uSource,vUv).rgb+texture2D(uHalo,vUv).rgb*uStrength;\n gl_FragColor=vec4(uBackground+c,1.);return;}\n if(uPass\u003c.5){\n vec3 c=(readLight(vUv+uStep*vec2(-.5,-.5))+readLight(vUv+uStep*vec2(.5,-.5))\n +readLight(vUv+uStep*vec2(-.5,.5))+readLight(vUv+uStep*vec2(.5,.5)))*.25;\n c*=smoothstep(.16,.65,max(c.r,max(c.g,c.b)));gl_FragColor=vec4(c,1.);return;}\n vec3 c=readLight(vUv)*.227027;\n c+=(readLight(vUv+uStep*1.384615)+readLight(vUv-uStep*1.384615))*.316216;\n c+=(readLight(vUv+uStep*3.230769)+readLight(vUv-uStep*3.230769))*.070270;\n gl_FragColor=vec4(c,1.);\n}";
const TRACKS = [[[[128,2],[65,24],[11,93],[10,162]],[[10,162],[9,249],[79,263],[88,263]],[[88,263],[117,263],[136,244],[136,221]],[[136,221],[136,190],[103,194],[103,209]]],[[[225,32],[132,4],[30,76],[30,160]],[[30,160],[30,248],[98,260],[126,248]]],[[[126,215],[125,223],[117,237],[101,236]],[[101,236],[89,236],[76,220],[76,196]],[[76,196],[76,166],[100,143],[132,143]],[[132,143],[172,143],[213,171],[213,231]],[[213,231],[213,276],[170,328],[86,317]]],[[[107,237],[82,240],[61,223],[61,185]],[[61,185],[61,153],[92,123],[132,123]],[[132,123],[173,123],[228,150],[228,225]],[[228,225],[228,287],[168,322],[121,322]],[[121,322],[53,322],[11,265],[2,208]]],[[[115,211],[115,226],[92,227],[92,205]],[[92,205],[92,184],[110,175],[128,175]],[[128,175],[146,175],[174,196],[174,231]],[[174,231],[174,264],[148,282],[134,287]],[[134,287],[119,293],[82,297],[54,269]]]];
const TRACK_SPEEDS = [0.025,0.018,0.021,0.016,0.03];
const TRACK_DEPTH = [0.62,-0.46,0.78,-0.7,0.42];
const TRACK_SAMPLES = 512;

const canvas = document.getElementById('particle-canvas');
const errorBox = document.getElementById('error');
document.body.style.background = CONFIG.background;

function showError(message) {
  errorBox.textContent = message;
  errorBox.style.display = message ? 'block' : 'none';
}

function rgb(hex) {
  return [1, 3, 5].map(function (index) {
    return parseInt(hex.slice(index, index + 2), 16) / 255;
  });
}

function seededRandom(seed) {
  return function () {
    seed |= 0;
    seed = (seed + 0x6d2b79f5) | 0;
    let value = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    value = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;
    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
  };
}

function pointAt(curve, t) {
  const segment = Math.min(curve.length - 1, Math.floor(t * curve.length));
  const u = t * curve.length - segment;
  const v = 1 - u;
  const p = curve[segment];
  return [
    v * v * v * p[0][0] + 3 * v * v * u * p[1][0] + 3 * v * u * u * p[2][0] + u * u * u * p[3][0],
    v * v * v * p[0][1] + 3 * v * v * u * p[1][1] + 3 * v * u * u * p[2][1] + u * u * u * p[3][1]
  ];
}

function buildTracks(aspect, galaxy) {
  const fit = Math.min(1, aspect / 0.84);
  return TRACKS.map(function (curve, lane) {
    const table = new Float32Array((TRACK_SAMPLES + 1) * 5);
    const raw = [];
    const lengths = [0];
    for (let index = 0; index <= 2048; index += 1) {
      const point = pointAt(curve, index / 2048);
      raw.push(point);
      if (index) {
        lengths.push(lengths[index - 1] + Math.hypot(point[0] - raw[index - 1][0], point[1] - raw[index - 1][1]));
      }
    }
    const radial = function (point) { return Math.hypot(point[0] - 115, point[1] - 211); };
    const reverse = radial(raw[0]) < radial(raw[2048]);
    for (let index = 0; index <= TRACK_SAMPLES; index += 1) {
      const fraction = index / TRACK_SAMPLES;
      const progress = reverse ? 1 - fraction : fraction;
      const distance = progress * lengths[2048];
      let low = 0;
      let high = 2048;
      while (high - low > 1) {
        const middle = (low + high) >> 1;
        if (lengths[middle] < distance) low = middle;
        else high = middle;
      }
      const amount = (distance - lengths[low]) / (lengths[high] - lengths[low] || 1);
      const x = raw[low][0] + (raw[high][0] - raw[low][0]) * amount;
      const y = raw[low][1] + (raw[high][1] - raw[low][1]) * amount;
      if (galaxy) {
        const radius = 0.48 * Math.pow(1 - fraction, 0.78);
        const angle = lane * Math.PI * 2 / 5 + radius * 16;
        table[index * 5] = Math.cos(angle) * radius * fit;
        table[index * 5 + 1] = Math.sin(angle) * radius * 0.75 * fit;
      } else {
        table[index * 5] = (x - 115) / 325 * 0.9 * fit;
        table[index * 5 + 1] = ((y - 211) / 325 * 0.9 + 0.11) * fit;
      }
      table[index * 5 + 2] = Math.sin(progress * Math.PI) * Math.sin(progress * Math.PI * 1.35 + lane * 0.82) * TRACK_DEPTH[lane] * 0.09;
    }
    for (let index = 0; index <= TRACK_SAMPLES; index += 1) {
      const a = Math.max(0, index - 1) * 5;
      const b = Math.min(TRACK_SAMPLES, index + 1) * 5;
      const dx = table[b] - table[a];
      const dy = table[b + 1] - table[a + 1];
      const length = Math.hypot(dx, dy) || 1;
      table[index * 5 + 3] = -dy / length;
      table[index * 5 + 4] = dx / length;
    }
    return table;
  });
}

function smooth(a, b, value) {
  const amount = Math.min(1, Math.max(0, (value - a) / (b - a)));
  return amount * amount * (3 - 2 * amount);
}

function advanceOrbit(state, seconds, points, activity) {
  for (let index = 0; index < state.lanes.length; index += 1) {
    const offset3 = index * 3;
    const lane = state.lanes[index];
    activity[offset3] = 1;
    activity[offset3 + 1] = 1;
    activity[offset3 + 2] = 0;
    if (lane === -1 || lane === -3) {
      points[offset3] = state.base[offset3];
      points[offset3 + 1] = state.base[offset3 + 1];
      points[offset3 + 2] = state.base[offset3 + 2];
      continue;
    }
    if (lane === -2) {
      const progress = (state.phases[index] + seconds * 0.045) % 1;
      const radius = 0.045 * Math.pow(1 - progress, 0.7) * state.fit;
      const angle = state.offsets[index * 2] - progress * Math.PI * 4;
      points[offset3] = Math.cos(angle) * radius;
      points[offset3 + 1] = state.centerY + Math.sin(angle) * radius;
      points[offset3 + 2] = state.offsets[index * 2 + 1] * (1 - progress);
      activity[offset3] = smooth(0, 0.06, progress) * (1 - smooth(0.94, 1, progress));
      activity[offset3 + 1] = 0.75;
      activity[offset3 + 2] = 1;
      continue;
    }
    const phase = (state.phases[index] + seconds * TRACK_SPEEDS[lane]) % 1;
    const progress = phase + 0.34 * Math.sin(phase * Math.PI * 2) / (Math.PI * 2);
    const sample = progress * TRACK_SAMPLES;
    const sampleIndex = Math.min(TRACK_SAMPLES - 1, Math.floor(sample));
    const amount = sample - sampleIndex;
    const a = sampleIndex * 5;
    const b = a + 5;
    const table = state.tracks[lane];
    const nx = table[a + 3] + (table[b + 3] - table[a + 3]) * amount;
    const ny = table[a + 4] + (table[b + 4] - table[a + 4]) * amount;
    const laneOffset = state.offsets[index * 2];
    points[offset3] = table[a] + (table[b] - table[a]) * amount + nx * laneOffset;
    points[offset3 + 1] = table[a + 1] + (table[b + 1] - table[a + 1]) * amount + ny * laneOffset;
    points[offset3 + 2] = table[a + 2] + (table[b + 2] - table[a + 2]) * amount + state.offsets[index * 2 + 1];
    activity[offset3] = smooth(0, 0.055, progress) * (1 - smooth(0.945, 1, progress));
    activity[offset3 + 1] = 0.3 + 0.7 * Math.pow(Math.sin(progress * Math.PI), 0.68);
    activity[offset3 + 2] = Math.max(0, 1 - Math.hypot(points[offset3], points[offset3 + 1] - state.centerY) / 0.1);
  }
}

function createOrbit(count, aspect, random, galaxy) {
  const points = new Float32Array(count * 3);
  const colors = new Float32Array(count * 3);
  const activity = new Float32Array(count * 3);
  const fit = Math.min(1, aspect / 0.84);
  const centerY = galaxy ? 0 : 0.11 * fit;
  const state = {
    tracks: buildTracks(aspect, galaxy),
    lanes: new Int8Array(count),
    phases: new Float32Array(count),
    offsets: new Float32Array(count * 2),
    base: new Float32Array(count * 3),
    centerY: centerY,
    fit: fit
  };
  let lane = 0;
  for (let index = 0; index < count; index += 1) {
    state.phases[index] = random();
    if (index >= count - 4) {
      state.lanes[index] = -3;
      const angle = (index - count + 4) * Math.PI * 2 / 3;
      const radius = index === count - 1 ? 0 : 0.003;
      points.set([Math.cos(angle) * radius, centerY + Math.sin(angle) * radius, 0], index * 3);
    } else if (index % 10 === 0) {
      state.lanes[index] = -1;
      points.set([(random() - 0.5) * aspect * 1.8, (random() - 0.5) * 1.8, (random() - 0.5) * 0.7], index * 3);
    } else if (index % 71 === 1) {
      state.lanes[index] = -2;
      state.offsets[index * 2] = random() * Math.PI * 2;
      state.offsets[index * 2 + 1] = (random() - 0.5) * 0.035;
    } else {
      state.lanes[index] = lane % 5;
      lane += 1;
      state.offsets[index * 2] = (random() + random() - 1) * 0.01 * fit;
      state.offsets[index * 2 + 1] = (random() + random() - 1) * 0.02;
    }
  }
  state.base.set(points);
  advanceOrbit(state, 0, points, activity);
  return { points: points, colors: colors, activity: activity, orbit: state };
}

async function makeTargets(config, aspect, signal) {
  const random = seededRandom(73124);
  if (config.mode === 'six' || config.mode === 'galaxy') {
    return createOrbit(config.count, aspect, random, config.mode === 'galaxy');
  }
  const points = new Float32Array(config.count * 3);
  const colors = new Float32Array(config.count * 3);
  const samples = [];
  const mask = document.createElement('canvas');
  mask.width = 900;
  mask.height = 600;
  const context = mask.getContext('2d', { willReadFrequently: true });
  if (!context) throw new Error('Could not create the image sampling canvas.');

  if (config.mode === 'text') {
    const lines = (config.text.trim() || 'HELLO').split('\n').slice(0, 4);
    context.font = '700 180px Arial, "PingFang SC", sans-serif';
    const widest = Math.max.apply(null, lines.map(function (line) { return context.measureText(line).width; }));
    const fontSize = Math.min(210, 790 / Math.max(widest, 1) * 180, 460 / lines.length);
    context.font = '700 ' + fontSize + 'px Arial, "PingFang SC", sans-serif';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.fillStyle = '#ffffff';
    lines.forEach(function (line, index) {
      context.fillText(line, 450, 300 + (index - (lines.length - 1) / 2) * fontSize * 1.12);
    });
  } else {
    if (!config.image) throw new Error('Choose an image or enter an image URL.');
    const image = new Image();
    image.crossOrigin = 'anonymous';
    await new Promise(function (resolve, reject) {
      const cancel = function () {
        cleanup();
        image.src = '';
        reject(new DOMException('Aborted', 'AbortError'));
      };
      const timeout = window.setTimeout(function () {
        cleanup();
        image.src = '';
        reject(new Error('Image loading timed out. Try another image.'));
      }, 15000);
      const cleanup = function () {
        window.clearTimeout(timeout);
        signal.removeEventListener('abort', cancel);
      };
      image.onload = function () { cleanup(); resolve(); };
      image.onerror = function () {
        cleanup();
        reject(new Error('Could not read this image. Its URL must allow cross-origin access.'));
      };
      signal.addEventListener('abort', cancel, { once: true });
      if (signal.aborted) cancel();
      else image.src = config.image;
    });
    if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
    const ratio = Math.min(820 / image.width, 520 / image.height);
    context.drawImage(image, (900 - image.width * ratio) / 2, (600 - image.height * ratio) / 2, image.width * ratio, image.height * ratio);
  }

  let pixels;
  try {
    pixels = context.getImageData(0, 0, 900, 600).data;
  } catch (error) {
    throw new Error('This image blocks cross-origin sampling. Download it and use a local or CORS-enabled image.');
  }
  const fit = Math.min(1, aspect / 1.55);
  for (let y = 0; y < 600; y += 2) {
    for (let x = 0; x < 900; x += 2) {
      const pixel = (y * 900 + x) * 4;
      const luminance = pixels[pixel] * 0.2126 + pixels[pixel + 1] * 0.7152 + pixels[pixel + 2] * 0.0722;
      if (pixels[pixel + 3] > 50 && (config.mode === 'text' || (config.invert ? 255 - luminance : luminance) > config.threshold)) {
        samples.push([
          (x / 900 - 0.5) * 1.38 * fit,
          (y / 600 - 0.5) * 0.92 * fit,
          pixels[pixel] / 255,
          pixels[pixel + 1] / 255,
          pixels[pixel + 2] / 255
        ]);
      }
    }
  }
  if (!samples.length) throw new Error('No particles found. Lower the brightness threshold or toggle Invert brightness.');

  for (let index = 0; index < config.count; index += 1) {
    let x = 0;
    let y = 0;
    let z = 0;
    if (index % 10 === 0) {
      x = (random() - 0.5) * aspect * 1.8;
      y = (random() - 0.5) * 1.8;
      z = (random() - 0.5) * 0.9;
    } else {
      const sample = samples[Math.floor(random() * samples.length)];
      x = sample[0] + (random() - 0.5) * 0.002;
      y = sample[1] + (random() - 0.5) * 0.002;
      z = (random() - 0.5) * 0.05;
      if (config.mode === 'image' && config.imageColors) colors.set(sample.slice(2), index * 3);
    }
    points.set([x, y, z], index * 3);
  }
  return { points: points, colors: colors, activity: new Float32Array(config.count * 3).fill(1), orbit: null };
}

function compileShader(gl, type, source) {
  const shader = gl.createShader(type);
  if (!shader) throw new Error('Could not create a WebGL shader.');
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const message = gl.getShaderInfoLog(shader) || 'Shader compilation failed.';
    gl.deleteShader(shader);
    throw new Error(message);
  }
  return shader;
}

function createBloom(gl) {
  const shaders = [];
  const textures = [];
  const frames = [];
  const program = gl.createProgram();
  const quad = gl.createBuffer();
  if (!program || !quad) throw new Error('Could not create the particle glow renderer.');
  const dispose = function () {
    shaders.forEach(function (shader) { gl.deleteShader(shader); });
    textures.forEach(function (texture) { gl.deleteTexture(texture); });
    frames.forEach(function (frame) { gl.deleteFramebuffer(frame); });
    gl.deleteBuffer(quad);
    gl.deleteProgram(program);
  };
  try {
    [[gl.VERTEX_SHADER, BLOOM_VERTEX_SHADER], [gl.FRAGMENT_SHADER, BLOOM_FRAGMENT_SHADER]].forEach(function (entry) {
      const shader = compileShader(gl, entry[0], entry[1]);
      shaders.push(shader);
      gl.attachShader(program, shader);
    });
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error('Glow shader link failed.');
    for (let index = 0; index < 3; index += 1) {
      const texture = gl.createTexture();
      const frame = gl.createFramebuffer();
      if (!texture || !frame) throw new Error('Could not allocate the particle glow canvas.');
      textures.push(texture);
      frames.push(frame);
    }
    gl.bindBuffer(gl.ARRAY_BUFFER, quad);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
  } catch (error) {
    dispose();
    throw error;
  }
  const position = gl.getAttribLocation(program, 'aPosition');
  const uniforms = {};
  ['Source', 'Halo', 'Step', 'Background', 'Pass', 'Strength'].forEach(function (name) {
    uniforms[name] = gl.getUniformLocation(program, 'u' + name);
  });
  let width = 1;
  let height = 1;
  let bloomWidth = 1;
  let bloomHeight = 1;
  return {
    resize: function (nextWidth, nextHeight) {
      width = nextWidth;
      height = nextHeight;
      bloomWidth = Math.max(1, Math.ceil(nextWidth / 2));
      bloomHeight = Math.max(1, Math.ceil(nextHeight / 2));
      for (let index = 0; index < 3; index += 1) {
        gl.bindTexture(gl.TEXTURE_2D, textures[index]);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, index ? bloomWidth : width, index ? bloomHeight : height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        gl.bindFramebuffer(gl.FRAMEBUFFER, frames[index]);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textures[index], 0);
        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) throw new Error('Could not allocate the particle glow canvas.');
      }
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    },
    begin: function () {
      gl.bindFramebuffer(gl.FRAMEBUFFER, frames[0]);
      gl.viewport(0, 0, width, height);
      gl.enable(gl.BLEND);
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
    },
    end: function (background, strength) {
      gl.disable(gl.BLEND);
      gl.useProgram(program);
      gl.bindBuffer(gl.ARRAY_BUFFER, quad);
      gl.enableVertexAttribArray(position);
      gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
      gl.uniform1i(uniforms.Source, 0);
      gl.uniform1i(uniforms.Halo, 1);
      gl.uniform3fv(uniforms.Background, background);
      gl.uniform1f(uniforms.Strength, strength * 1.5);
      gl.activeTexture(gl.TEXTURE1);
      gl.bindTexture(gl.TEXTURE_2D, textures[0]);
      for (let pass = 0; pass < 4; pass += 1) {
        gl.uniform1f(uniforms.Pass, pass);
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, textures[pass === 1 ? 1 : pass === 2 ? 2 : 0]);
        gl.bindFramebuffer(gl.FRAMEBUFFER, pass === 3 ? null : frames[pass === 1 ? 2 : 1]);
        gl.viewport(0, 0, pass === 3 ? width : bloomWidth, pass === 3 ? height : bloomHeight);
        gl.uniform2f(
          uniforms.Step,
          pass === 0 ? 1 / width : pass === 1 ? 1 / bloomWidth : 0,
          pass === 0 ? 1 / height : pass === 2 ? 1 / bloomHeight : 0
        );
        if (pass === 3) {
          gl.activeTexture(gl.TEXTURE1);
          gl.bindTexture(gl.TEXTURE_2D, textures[1]);
        }
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
      }
      gl.activeTexture(gl.TEXTURE0);
    },
    dispose: dispose
  };
}

async function start() {
  const gl = canvas.getContext('webgl', {
    alpha: false,
    antialias: false,
    preserveDrawingBuffer: true,
    powerPreference: 'high-performance'
  });
  if (!gl) throw new Error('WebGL is unavailable. Enable hardware acceleration and try again.');

  const particleProgram = gl.createProgram();
  if (!particleProgram) throw new Error('Could not create the particle renderer.');
  const particleShaders = [
    compileShader(gl, gl.VERTEX_SHADER, PARTICLE_VERTEX_SHADER),
    compileShader(gl, gl.FRAGMENT_SHADER, PARTICLE_FRAGMENT_SHADER)
  ];
  particleShaders.forEach(function (shader) { gl.attachShader(particleProgram, shader); });
  gl.linkProgram(particleProgram);
  if (!gl.getProgramParameter(particleProgram, gl.LINK_STATUS)) {
    throw new Error(gl.getProgramInfoLog(particleProgram) || 'Could not link the particle renderer.');
  }
  const bloom = createBloom(gl);
  gl.useProgram(particleProgram);
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

  const uniforms = {};
  ['Resolution', 'Pointer', 'Time', 'Morph', 'Size', 'Scale', 'Scatter', 'Depth', 'Interaction', 'Rotation', 'Dpr', 'Intro', 'Color', 'Accent', 'Glow', 'MaxPoint', 'Brilliance', 'StarDensity', 'Twinkle', 'Footprint', 'StarRays', 'RayLength', 'CoreGlow', 'Orbit', 'DensityScale'].forEach(function (name) {
    uniforms[name] = gl.getUniformLocation(particleProgram, 'u' + name);
  });
  const buffers = {};
  const attributes = {};
  ['From', 'Target', 'Color', 'Seed', 'Activity', 'RayMask'].forEach(function (name) {
    const buffer = gl.createBuffer();
    if (!buffer) throw new Error('Could not allocate particle data.');
    buffers[name] = buffer;
    attributes[name] = gl.getAttribLocation(particleProgram, 'a' + name);
  });
  const bindAttributes = function () {
    Object.keys(buffers).forEach(function (name) {
      gl.bindBuffer(gl.ARRAY_BUFFER, buffers[name]);
      gl.enableVertexAttribArray(attributes[name]);
      gl.vertexAttribPointer(attributes[name], 3, gl.FLOAT, false, 0, 0);
    });
  };
  const upload = function (name, data) {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers[name]);
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
  };

  let width = 1;
  let height = 1;
  let dpr = 1;
  let count = 0;
  let time = 0;
  let flowTime = 0;
  let intro = CONFIG.paused ? 1 : 0;
  let last = 0;
  let disposed = false;
  let target = new Float32Array(0);
  let activity = new Float32Array(0);
  let orbit = null;
  let pointer = [10, 10];
  let smoothed = [0, 0];
  let abort = new AbortController();
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
  const maxPoint = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE)[1];

  const rebuild = async function () {
    abort.abort();
    abort = new AbortController();
    const signal = abort.signal;
    try {
      const result = await makeTargets(CONFIG, width / height, signal);
      if (disposed || signal.aborted) return;
      target = result.points;
      activity = result.activity;
      orbit = result.orbit;
      flowTime = 0;
      count = result.points.length / 3;
      const seeds = new Float32Array(count * 3);
      for (let index = 0; index < count * 3; index += 1) {
        seeds[index] = ((Math.sin(index * 127.1 + 311.7) * 43758.5453 % 1) + 1) % 1;
      }
      for (let index = 0; index < count; index += 10) {
        if (index % 300 !== 0) seeds[index * 3 + 2] *= 0.83;
      }
      if (orbit) {
        for (let index = count - 4; index < count - 1; index += 1) seeds.set([0.4, 0.85, 0.9999], index * 3);
        seeds[count * 3 - 1] = 2;
      }
      const rayMask = new Float32Array(count * 3);
      const candidates = new Array(orbit ? 5 : 3).fill(-1);
      for (let index = 0; index < count; index += 1) {
        const lane = orbit ? orbit.lanes[index] : index % 3;
        if (lane < 0 || index % 10 === 0) continue;
        const previous = candidates[lane];
        if (previous < 0 || seeds[index * 3 + 2] > seeds[previous * 3 + 2]) candidates[lane] = index;
      }
      candidates.forEach(function (index) { if (index >= 0) rayMask[index * 3] = 1; });
      upload('From', target);
      upload('Target', target);
      upload('Color', result.colors);
      upload('Seed', seeds);
      upload('Activity', activity);
      upload('RayMask', rayMask);
      showError('');
    } catch (error) {
      if (error && error.name !== 'AbortError') showError(error instanceof Error ? error.message : 'Could not create the particle scene.');
    }
  };

  const resize = function () {
    const bounds = canvas.getBoundingClientRect();
    const oldAspect = width / height;
    width = Math.max(1, bounds.width);
    height = Math.max(1, bounds.height);
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(width * dpr);
    canvas.height = Math.round(height * dpr);
    bloom.resize(canvas.width, canvas.height);
    if (Math.abs(oldAspect - width / height) > 0.025 || !count) rebuild();
  };
  const resizeObserver = new ResizeObserver(resize);
  resizeObserver.observe(canvas);
  const move = function (event) {
    const bounds = canvas.getBoundingClientRect();
    pointer = [(event.clientX - bounds.left - width / 2) / height, (event.clientY - bounds.top - height / 2) / height];
  };
  const leave = function () { pointer = [10, 10]; };
  canvas.addEventListener('pointermove', move);
  canvas.addEventListener('pointerleave', leave);
  canvas.addEventListener('webglcontextlost', function (event) {
    event.preventDefault();
    showError('The graphics context was interrupted. Refresh the page to restore it.');
  });
  resize();

  const tick = function (now) {
    if (disposed) return;
    requestAnimationFrame(tick);
    const delta = Math.min((now - (last || now)) / 1000, 0.05);
    last = now;
    if (document.hidden || gl.isContextLost()) return;
    if (!CONFIG.paused && !reduced.matches) {
      time += delta * CONFIG.speed;
      flowTime += delta * CONFIG.speed * CONFIG.flowSpeed;
    }
    if (!CONFIG.paused) intro = Math.min(1, intro + delta / 2.8);
    if (reduced.matches) intro = 1;
    const amount = 1 - Math.exp(-delta * 4);
    if (!CONFIG.paused) {
      smoothed = smoothed.map(function (value, index) {
        return value + ((pointer[index] === 10 ? 0 : pointer[index]) - value) * amount;
      });
    }
    if (!count) return;
    if (orbit) {
      advanceOrbit(orbit, flowTime, target, activity);
      gl.bindBuffer(gl.ARRAY_BUFFER, buffers.Target);
      gl.bufferSubData(gl.ARRAY_BUFFER, 0, target);
      gl.bindBuffer(gl.ARRAY_BUFFER, buffers.Activity);
      gl.bufferSubData(gl.ARRAY_BUFFER, 0, activity);
    }
    bloom.begin();
    gl.useProgram(particleProgram);
    bindAttributes();
    gl.uniform2f(uniforms.Resolution, width, height);
    gl.uniform2f(uniforms.Pointer, pointer[0] === 10 ? 10 : smoothed[0], pointer[1] === 10 ? 10 : smoothed[1]);
    const values = {
      Time: time,
      Morph: 1,
      Size: CONFIG.size,
      Scale: CONFIG.scale,
      Scatter: CONFIG.scatter,
      Depth: CONFIG.depth,
      Interaction: reduced.matches ? 0 : CONFIG.interaction,
      Rotation: CONFIG.rotation * Math.PI / 180,
      Dpr: dpr,
      Intro: intro,
      Glow: CONFIG.glow,
      MaxPoint: maxPoint,
      Brilliance: CONFIG.brilliance,
      StarDensity: CONFIG.starDensity,
      Twinkle: CONFIG.twinkle,
      Footprint: orbit ? 1 : 0.72,
      StarRays: CONFIG.starRays,
      RayLength: CONFIG.rayLength,
      CoreGlow: CONFIG.coreGlow,
      Orbit: orbit ? 1 : 0,
      DensityScale: Math.min(1, (orbit ? 4500 : 6000) / count)
    };
    Object.keys(values).forEach(function (name) { gl.uniform1f(uniforms[name], values[name]); });
    gl.uniform3fv(uniforms.Color, rgb(CONFIG.color));
    gl.uniform3fv(uniforms.Accent, rgb(CONFIG.accentColor));
    gl.drawArrays(gl.POINTS, 0, count);
    bloom.end(rgb(CONFIG.background), CONFIG.glow);
  };
  requestAnimationFrame(tick);

  window.addEventListener('beforeunload', function () {
    disposed = true;
    abort.abort();
    resizeObserver.disconnect();
    canvas.removeEventListener('pointermove', move);
    canvas.removeEventListener('pointerleave', leave);
    Object.keys(buffers).forEach(function (name) { gl.deleteBuffer(buffers[name]); });
    particleShaders.forEach(function (shader) { gl.deleteShader(shader); });
    gl.deleteProgram(particleProgram);
    bloom.dispose();
  }, { once: true });
}

start().catch(function (error) {
  showError(error instanceof Error ? error.message : 'Unable to start the particle scene.');
  console.error(error);
});

  </script>
</body>
</html>
```

Download HTMLCopy code

> Full page index: [/llms.txt](/llms.txt)
