implement hueByDistance else .. to get line hue from time of line exist

<body>
<label>
  <input type="checkbox" id="hueByDistance" checked onchange="hueByDistance = this.checked">
  <span style="color: white;">hueByDistance</span>
</label>
<canvas id="gpu-canvas"></canvas>


<script type="module">
  // 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
  let hueByDistance=true; // if false get hue from time of line exist. (0 sec blue, to 5sec red;)
  
  // 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',
  });


  // 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,
  });

// Each line has 2 vertices, each vertex gets a vec4<f32> color
const lineColorBuffer = device.createBuffer({
  size: LINE_VERTICES * 4 * Float32Array.BYTES_PER_ELEMENT,
  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;
@group(0) @binding(3) var<storage, read_write> colors  : array<vec4<f32>>;

// helper: simple HSL->RGB (h∈[0,1], s=1.0, l=0.5)
fn hue2rgb(p: f32, q: f32, t: f32) -> f32 {
  var tt = t;
  if (tt < 0.0) { tt = tt + 1.0; }
  if (tt > 1.0) { tt = tt - 1.0; }
  if (tt < 1.0/6.0) { return p + (q - p) * 6.0 * tt; }
  if (tt < 1.0/2.0) { return q; }
  if (tt < 2.0/3.0) { return p + (q - p) * (2.0/3.0 - tt) * 6.0; }
  return p;
}

fn hsl2rgb(h: f32, s: f32, l: f32) -> vec3<f32> {
  if (s == 0.0) {
    return vec3<f32>(l, l, l);
  }
  let q = select(l * (1.0 + s), l + s - l*s, l < 0.5);
  let p = 2.0 * l - q;
  let r = hue2rgb(p, q, h + 1.0/3.0);
  let g = hue2rgb(p, q, h);
  let b = hue2rgb(p, q, h - 1.0/3.0);
  return vec3<f32>(r, g, b);
}

@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 = 1e6;
  var nearest: 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 d = distance(p, q);
    if (d < minDist) {
      minDist = d;
      nearest = j;
    }
  }

  // write endpoints
  let a = p;
  let b = particles.positions[nearest];
  lines[i * 2u]     = a;
  lines[i * 2u + 1u] = b;

  // now compute hue − map minDist from [redThresh, maxPossible]→[0,1]
  let redThresh = 0.05;                // ~10px in normalized coords, tweak to suit
  let maxDist   = 0.1; // 2 * 1.41421356;    // longest across [-1,1]×[-1,1]
  let t = clamp((minDist - redThresh) / (maxDist - redThresh), 0.0, 1.0);
  // hue: 0°→240° = 0.0→(240/360)=2/3
  let h = t * (2.0/3.0);
  let rgb = hsl2rgb(h, 1.0, 0.5);
  let col = vec4<f32>(rgb, 1.0);

  // write same color into both vertices
  colors[i * 2u]     = col;
  colors[i * 2u + 1u] = col;
}
`;

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

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

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

const vertexShaderLines = `
struct VSOut {
  @builtin(position) Position : vec4<f32>,
  @location(0) col : vec4<f32>
};

@vertex
fn main(@location(0) position : vec2<f32>,
         @location(1) color    : vec4<f32>) -> VSOut {
  var out: VSOut;
  out.Position = vec4<f32>(position, 0.0, 1.0);
  out.col      = color;
  return out;
}
`;

const fragmentShaderLines = `
@fragment
fn main(@location(0) col : vec4<f32>) -> @location(0) vec4<f32> {
  return col;
}
`;


  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 });

  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 } },
    { binding: 3, resource: { buffer: lineColorBuffer } }, 
  ],
});


  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',
    },
  });

const renderPipelineLines = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: vertexModuleLines,
    entryPoint: 'main',
    buffers: [
      {
        arrayStride: LINE_VERTEX_SIZE,
        attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }],
      },
      {
        arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT,
        attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x4' }],
      },
    ],
  },
  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();


  function loop() {
    // 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);

    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.setVertexBuffer(1, lineColorBuffer);
	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(loop);
  }

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