change code make line hue from line length 10 for red to max length for blue; think where better to calculate color for best performance.


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebGPU Particles + Lines</title>
<style>
  body { margin: 0; overflow: hidden; background: #111; }
  canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="gpu-canvas"></canvas>
<script type="module">
  // Check WebGPU support
  if (!navigator.gpu) {
    alert("WebGPU not supported on this browser.");
    throw new Error("WebGPU not supported");
  }

  const canvas = document.getElementById('gpu-canvas');
  const adapter = await navigator.gpu.requestAdapter();
  const device = await adapter.requestDevice();
  const context = canvas.getContext('webgpu');

  // Configure canvas context
  const format = navigator.gpu.getPreferredCanvasFormat();
  context.configure({
    device,
    format,
    alphaMode: 'opaque',
  });

  // Constants
  const PARTICLE_COUNT = 50;
  const PARTICLE_SIZE = 4 * 2; // vec2<f32> position
  const LINE_VERTEX_SIZE = 4 * 2; // vec2<f32> position
  const LINE_COUNT = PARTICLE_COUNT; // one line per particle (to nearest neighbor)
  const LINE_VERTICES = LINE_COUNT * 2; // 2 vertices per line

  // Particle data on CPU
  const particlesArray = new Float32Array(PARTICLE_COUNT * 2); // x,y per particle
  const velocitiesArray = new Float32Array(PARTICLE_COUNT * 2); // vx, vy per particle

  // Initialize particles randomly in [-1,1] range
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    particlesArray[i * 2] = (Math.random() * 2 - 1) * 0.9;
    particlesArray[i * 2 + 1] = (Math.random() * 2 - 1) * 0.9;
    velocitiesArray[i * 2] = (Math.random() * 2 - 1) * 0.002;
    velocitiesArray[i * 2 + 1] = (Math.random() * 2 - 1) * 0.002;
  }

  // Create GPU buffers
  // Particle positions buffer (storage & vertex)
  const particleBuffer = device.createBuffer({
    size: particlesArray.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true,
  });
  new Float32Array(particleBuffer.getMappedRange()).set(particlesArray);
  particleBuffer.unmap();

  // Particle velocities buffer (storage only)
  const velocityBuffer = device.createBuffer({
    size: velocitiesArray.byteLength,
    usage: GPUBufferUsage.STORAGE,
    mappedAtCreation: true,
  });
  new Float32Array(velocityBuffer.getMappedRange()).set(velocitiesArray);
  velocityBuffer.unmap();

  // Line vertex buffer (output from compute shader)
  // Each line has 2 vertices (vec2 positions)
  const lineVertexBuffer = device.createBuffer({
    size: LINE_VERTICES * LINE_VERTEX_SIZE,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE,
  });

  // Uniform buffer for particle count
  const uniformBuffer = device.createBuffer({
    size: 4,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
  device.queue.writeBuffer(uniformBuffer, 0, new Uint32Array([PARTICLE_COUNT]));

  // Compute shader WGSL: find nearest neighbor for each particle and output line vertices
  const computeShaderCode = `
struct Particles {
  positions: array<vec2<f32>>
};

@group(0) @binding(0) var<storage, read> particles : Particles;
@group(0) @binding(1) var<storage, read_write> lines : array<vec2<f32>>;
@group(0) @binding(2) var<uniform> uCount : u32;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
  let i = global_id.x;
  if (i >= uCount) { return; }

  let p = particles.positions[i];
  var minDist = 10000.0;
  var nearestIndex : u32 = 0u;

  // Find nearest neighbor
  for (var j: u32 = 0u; j < uCount; j = j + 1u) {
    if (i == j) { continue; }
    let q = particles.positions[j];
    let dist = distance(p, q);
    if (dist < minDist) {
      minDist = dist;
      nearestIndex = j;
    }
  }

  // Write line vertices: 2 vertices per line
  // line index = i
  // vertex 0 = p, vertex 1 = nearest neighbor position
  lines[i * 2u] = p;
  lines[i * 2u + 1u] = particles.positions[nearestIndex];
}
`;

  // Render shader WGSL: draw points and lines
  // We'll use two pipelines: one for points, one for lines

  // Vertex shader for points
  const vertexShaderPoints = `
@vertex
fn main(@location(0) position: vec2<f32>) -> @builtin(position) vec4<f32> {
  return vec4<f32>(position, 0.0, 1.0);
}
`;

  // Fragment shader for points
  const fragmentShaderPoints = `
@fragment
fn main() -> @location(0) vec4<f32> {
  return vec4<f32>(1.0, 0.8, 0.2, 1.0);
}
`;

  // Vertex shader for lines
  const vertexShaderLines = `
@vertex
fn main(@location(0) position: vec2<f32>) -> @builtin(position) vec4<f32> {
  return vec4<f32>(position, 0.0, 1.0);
}
`;

  // Fragment shader for lines
  const fragmentShaderLines = `
@fragment
fn main() -> @location(0) vec4<f32> {
  return vec4<f32>(0.2, 0.8, 1.0, 1.0);
}
`;

  // Create shader modules
  const computeModule = device.createShaderModule({ code: computeShaderCode });
  const vertexModulePoints = device.createShaderModule({ code: vertexShaderPoints });
  const fragmentModulePoints = device.createShaderModule({ code: fragmentShaderPoints });
  const vertexModuleLines = device.createShaderModule({ code: vertexShaderLines });
  const fragmentModuleLines = device.createShaderModule({ code: fragmentShaderLines });

  // Compute pipeline
  const computePipeline = device.createComputePipeline({
    layout: 'auto',
    compute: {
      module: computeModule,
      entryPoint: 'main',
    },
  });

  // Bind group for compute
  const computeBindGroup = device.createBindGroup({
    layout: computePipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: { buffer: particleBuffer } },
      { binding: 1, resource: { buffer: lineVertexBuffer } },
      { binding: 2, resource: { buffer: uniformBuffer } },
    ],
  });

  // Render pipeline for points
  const renderPipelinePoints = device.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: vertexModulePoints,
      entryPoint: 'main',
      buffers: [
        {
          arrayStride: PARTICLE_SIZE,
          attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }],
        },
      ],
    },
    fragment: {
      module: fragmentModulePoints,
      entryPoint: 'main',
      targets: [{ format }],
    },
    primitive: {
      topology: 'point-list',
    },
  });

  // Render pipeline for lines
  const renderPipelineLines = device.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: vertexModuleLines,
      entryPoint: 'main',
      buffers: [
        {
          arrayStride: LINE_VERTEX_SIZE,
          attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }],
        },
      ],
    },
    fragment: {
      module: fragmentModuleLines,
      entryPoint: 'main',
      targets: [{ format }],
    },
    primitive: {
      topology: 'line-list',
    },
  });

  // Resize canvas to fit window
  function resize() {
    const dpr = window.devicePixelRatio || 1;
    canvas.width = canvas.clientWidth * dpr;
    canvas.height = canvas.clientHeight * dpr;
  }
  window.addEventListener('resize', resize);
  resize();

  // Animation loop
  function frame() {
    // Update particle positions on CPU
    for (let i = 0; i < PARTICLE_COUNT; i++) {
      let x = particlesArray[i * 2];
      let y = particlesArray[i * 2 + 1];
      let vx = velocitiesArray[i * 2];
      let vy = velocitiesArray[i * 2 + 1];

      x += vx;
      y += vy;

      // Bounce off edges [-1,1]
      if (x < -1 || x > 1) {
        vx = -vx;
        x = Math.min(1, Math.max(-1, x));
      }
      if (y < -1 || y > 1) {
        vy = -vy;
        y = Math.min(1, Math.max(-1, y));
      }

      particlesArray[i * 2] = x;
      particlesArray[i * 2 + 1] = y;
      velocitiesArray[i * 2] = vx;
      velocitiesArray[i * 2 + 1] = vy;
    }

    // Upload updated particle positions to GPU
    device.queue.writeBuffer(particleBuffer, 0, particlesArray);

    // Command encoder
    const commandEncoder = device.createCommandEncoder();

    // Compute pass: find nearest neighbors and write line vertices
    {
      const passEncoder = commandEncoder.beginComputePass();
      passEncoder.setPipeline(computePipeline);
      passEncoder.setBindGroup(0, computeBindGroup);
      passEncoder.dispatchWorkgroups(Math.ceil(PARTICLE_COUNT / 64));
      passEncoder.end();
    }

    // Render pass
    const textureView = context.getCurrentTexture().createView();
    const renderPassDescriptor = {
      colorAttachments: [
        {
          view: textureView,
          clearValue: { r: 0.05, g: 0.05, b: 0.1, a: 1 },
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],
    };

    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);

    // Draw lines
    passEncoder.setPipeline(renderPipelineLines);
    passEncoder.setVertexBuffer(0, lineVertexBuffer);
    passEncoder.draw(LINE_VERTICES);

    // Draw points
    passEncoder.setPipeline(renderPipelinePoints);
    passEncoder.setVertexBuffer(0, particleBuffer);
    passEncoder.draw(PARTICLE_COUNT);

    passEncoder.end();

    device.queue.submit([commandEncoder.finish()]);

    requestAnimationFrame(frame);
  }

  requestAnimationFrame(frame);
</script>
</body>
</html>