implement    Check if nearest neighbor changed

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

  
  // Add event listener to update the hueByDistance variable
document.getElementById('hueByDistance_el').addEventListener('change', (e) => {
  hueByDistance = e.target.checked;
  console.log('hueByDistance changed to:', hueByDistance);
});

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


const lineTimesBuffer = device.createBuffer({
  size: LINE_COUNT * Float32Array.BYTES_PER_ELEMENT,
  usage: GPUBufferUsage.STORAGE,
  mappedAtCreation: true,
});
// Initialize all times to current time
new Float32Array(lineTimesBuffer.getMappedRange()).fill(performance.now() / 1000);
lineTimesBuffer.unmap();

// Uniform buffer for time and settings
const timeUniformBuffer = device.createBuffer({
  size: 8, // float32 time + float32 hueByDistance
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});


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

struct TimeSettings {
  currentTime: f32,
  hueByDistance: 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>>;
@group(0) @binding(4) var<storage, read_write> lineTimes : array<f32>;
@group(0) @binding(5) var<uniform> timeSettings : TimeSettings;

// 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];
  
  // Check if nearest neighbor changed
  if (???) {
    lineTimes[i] = timeSettings.currentTime;
  }
  
  lines[i * 2u] = a;
  lines[i * 2u + 1u] = b;

  var h: f32;
  
  if (timeSettings.hueByDistance > 0.5) {
    // Distance-based coloring
  let redThresh = 0.05;                // ~10px in normalized coords, tweak to suit
  let maxDist   = 0.1; // 2 * 1.41421356;    is longest across [-1,1]×[-1,1]
    let t = clamp((minDist - redThresh) / (maxDist - redThresh), 0.0, 1.0);
    h = t * (2.0/3.0); // hue: 0°→240° = 0.0→(240/360)=2/3
  } else {
    // Time-based coloring
    let lineAge = timeSettings.currentTime - lineTimes[i];
    let maxAge = 5.0; // 5 seconds from blue to red
    let t = clamp(lineAge / maxAge, 0.0, 1.0);
    h = (1.0 - t) * (2.0/3.0); // Reverse mapping: newer = blue, older = red
  }

  let rgb = hsl2rgb(h, 1.0, 0.5);
  let col = vec4<f32>(rgb, 1.0);

  // 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 } },
    { binding: 4, resource: { buffer: lineTimesBuffer } },
    { binding: 5, resource: { buffer: timeUniformBuffer } },
  ],
});


  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);
	
	  // Update time uniform buffer
  const timeData = new Float32Array([
    performance.now() / 1000, // Current time in seconds
    hueByDistance ? 1.0 : 0.0 // Convert boolean to float
  ]);
  device.queue.writeBuffer(timeUniformBuffer, 0, timeData);
  //console.log('timeData:', Array.from(timeData), hueByDistance);

    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 renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
    // Draw lines
    renderPass.setPipeline(renderPipelineLines);
    renderPass.setVertexBuffer(0, lineVertexBuffer);
    renderPass.setVertexBuffer(1, lineColorBuffer);
    renderPass.draw(LINE_VERTICES);
    // Draw points
    renderPass.setPipeline(renderPipelinePoints);
    renderPass.setVertexBuffer(0, particleBuffer);
    renderPass.draw(PARTICLE_COUNT);
    renderPass.end();
    device.queue.submit([commandEncoder.finish()]);

    requestAnimationFrame(loop);
  }

  requestAnimationFrame(loop);