Code

function vec2distance (a, b) {
  return Math.hypot(a[0] - b[0], a[1] - b[1]);
}

const pixelRatio = window.devicePixelRatio;

const regl = createREGL({
  pixelRatio,
  extensions: ['ANGLE_instanced_arrays']
});

// Instantiate a command for drawing lines
const drawLines = reglLines(regl, {
  vert: `
    precision highp float;

    // Use a vec2 attribute to construt the vec4 vertex position
    #pragma lines: attribute vec2 xy;
    #pragma lines: position = getPosition(xy);
    vec4 getPosition(vec2 xy) {
      return vec4(xy, 0, 1);
    }

    // Pass the distance without modification as a varying
    #pragma lines: attribute float dist;
    #pragma lines: varying float dist = getDist(dist);
    float getDist(float dist) {
      return dist;
    }

    // Return the line width from a uniorm
    #pragma lines: width = getWidth();
    uniform float width;
    float getWidth() {
      return width;
    }`,
  frag: `
    precision lowp float;
    varying float dist;
    uniform float dashLength;

    float linearstep (float a, float b, float x) {
      return clamp((x - a) / (b - a), 0.0, 1.0);
    }

    void main () {
      float dashvar = fract(dist / dashLength) * dashLength;
      gl_FragColor = vec4(vec3(
        linearstep(0.0, 1.0, dashvar)
        * linearstep(dashLength * 0.5 + 1.0, dashLength * 0.5, dashvar)
      ), 1);
    }`,
  // Multiply the width by the pixel ratio for consistent width
  uniforms: {
    width: (ctx, props) => ctx.pixelRatio * props.width,
    dashLength: (ctx, props) => ctx.pixelRatio * props.width * props.dashLength * 2.0,
  },
  depth: { enable: true },
  cull: { enable: false }
});

// Construct an array of xy pairs
const n = 11;
const path = [...Array(n).keys()]
  .map(i => (i / (n - 1) * 2.0 - 1.0) * 0.8)
  .map(t => [t, 0.5 * Math.sin(8.0 * t)]);

function project(point) {
  return [
    (0.5 + 0.5 * point[0]) * regl._gl.canvas.width,
    (0.5 + 0.5 * point[1]) * regl._gl.canvas.height
  ];
}

function computeCumulativeDistance (dist, points, project) {
  let prevPoint = project(points[0]);
  for (let i = 1; i < points.length; i++) {
    const point = project(points[i]);
    const d =  dist[i - 1] + vec2distance(point, prevPoint);
    dist[i] = d;
    prevPoint = point;
  }
  return dist;
}

const dist = Array(path.length).fill(0);
const distBuffer = regl.buffer(dist);
const endpointDistBuffer = regl.buffer(new Float32Array(6));
const pathBuffer = regl.buffer(path);
const endpointBuffer = regl.buffer(new Float32Array(6));

// Set up the data to be drawn. Note that we preallocate buffers and don't create
// them on every draw call.
const lineData = {
  width: 40,
  dashLength: 4,
  join: 'round',
  cap: 'round',
  vertexCount: path.length,
  vertexAttributes: {
    xy: regl.buffer(path),
    dist: distBuffer
  },
  endpointCount: 2,
  endpointAttributes: {
    xy: regl.buffer([path.slice(0, 3), path.slice(-3).reverse()]),
    dist: endpointDistBuffer
  }
};

function updateBuffers () {
  lineData.vertexAttributes.dist.subdata(dist);
  lineData.endpointAttributes.dist.subdata([dist.slice(0, 3), dist.slice(-3).reverse()]);
  lineData.vertexAttributes.xy.subdata(path);
  lineData.endpointAttributes.xy.subdata([path.slice(0, 3), path.slice(-3).reverse()]);
}

function draw () {
  updateBuffers();

  regl.poll();
  regl.clear({color: [0.2, 0.2, 0.2, 1], depth: 1});
  drawLines(lineData);
}

window.addEventListener('mousemove', function (event) {
  const lastPoint = path[0];
  const newPoint = [
    event.offsetX / window.innerWidth * 2 - 1,
    -event.offsetY / window.innerHeight * 2 + 1
  ];
  const newDist = Math.hypot(
    window.innerWidth * (lastPoint[0] - newPoint[0]),
    window.innerHeight * (lastPoint[1] - newPoint[1])
  );
  if (newDist < Math.max(2, lineData.width * 0.5)) return;

  path.unshift(newPoint);
  dist.unshift(dist[0] - newDist);

  path.pop();
  dist.pop();

  draw();
});

computeCumulativeDistance(dist, path, project);
draw();

window.addEventListener('resize', function () {
  computeCumulativeDistance(dist, path, project);
  draw();
});