Code

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

// This example illustrates use of batching to reduce the number of shader program changes from
// thirty to just four--though the number of draw calls remains unchanged. This applies when each line
// has its own buffers and requires a separate draw call. Since some of the options (endpoints vs.
// interior segments and whether ot insert caps) need to be known at command *compile* time, it's
// possible to draw lines in a manner which switches the draw program many times.
//
// When using regl, you may invoke commands independently, e.g.
//
//    drawLines({...})
//    drawLines({...})
//    drawLines({...})
//
// or batched as an array, e.g.
//
//    drawLines([{...}, {...}, ...])
//
// The latter form allows regl to bypass some state change checks.
//
// This library performs the additional step of *reordering* draw commands to group together draw calls
// using the same shader program. It only applies when called with array of props. This option is
// *enabled* by default. It may be disabled by specifying `reorder = false`, avoided by independently
// invoking draw calls, or its potential side effects mitigated by using depth to enforce ordering of
// opaque objects.
const drawLines = reglLines(regl, {
  vert: `
    precision highp float;

    #pragma lines: attribute vec2 xy;
    #pragma lines: position = getPosition(xy);
    #pragma lines: width = getWidth();
    uniform float pixelRatio;

    vec4 getPosition(vec2 xy) {
      return vec4(xy, 0, 1);
    }
    float getWidth() { return 20.0 * pixelRatio; }
    vec2 getXY(vec2 xy) { return xy; }`,
  frag: `
    precision lowp float;
    uniform vec3 color;
    void main () {
      gl_FragColor = vec4(color, 0.7);
    }`,

  // Extra config passed to the draw command
  depth: { enable: false },
  cull: {enable: true, face: 'back'},
  blend: {
    enable: true,
    func: { srcRGB: "src alpha", srcAlpha: 1, dstRGB: "one minus src alpha", dstAlpha: 1 }
  },
  uniforms: {
    pixelRatio: regl.context('pixelRatio'),
    color: regl.prop('color')
  }
});

const n = 20;
const lineCount = 15;

function xy (line, i) {
  let t = (i / (n - 1) * 2 - 1) * 0.9;
  const y = ((line + 0.5) / lineCount * 2 - 1) * 0.9;
  return [t, y + 1.5 / lineCount * Math.sin((t - line * 0.2) * 30.0)];
}

// Construct independent sets of props, to be passed as an array and rendered in one pass.
// When called in this fashion, all lines using the same draw command will be batched together.
// The key compile-time variables affecting this are:
//   - insertCaps: insert caps to fill NaN gaps
//   - isEndpoints: true when rendering dedicated, separately-provided endpoint instances
// This may affect layering if depth is disabled. To get around this, you may simply invoke
// rendering independently for each line.
const lineList = [];
for (let line = 0; line < lineCount; line++) {
  const positions = [];
  for (let i = 0; i < n; i++) {
    positions.push(i === Math.floor(n / 2) ? [NaN, NaN] : xy(line, i));
  }
  lineList.push({
    color: [0, 1, 2].map(i => 0.5 + Math.cos((i / 3 + (line / lineCount)) * Math.PI * 2)),
    cap: line % 2 === 0 ? 'round' : 'square',
    join: line % 4 === 0 ? 'miter' : 'round',
    insertCaps: line % 3 === 0,
    vertexCount: positions.length,
    endpointCount: 2,
    vertexAttributes: {
      xy: regl.buffer(positions)
    },
    endpointAttributes: {
      xy: regl.buffer([positions.slice(0, 3), positions.slice(-3).reverse()])
    }
  });
}

function draw () {
  regl.poll();
  regl.clear({color: [0.2, 0.2, 0.2, 1]});
  drawLines(lineList);
}

draw();
window.addEventListener('resize', draw);