大量の点の 2 次元プロット（regl）

こんにちは。
大量の点の 2 次元プロットについて、[regl](https://github.com/regl-project/regl) (WebGL ライブラリ）を利用することによる高速性を試してみました（[regl](https://github.com/regl-project/regl)を利用する目的はWebGLを簡素に記述して利用したいためです）。mousedown, mouseup, wheel, resize イベントの処理については自前で行いました。

1000万点（ten millions）[^1] をプロットしてもスクロール・ズーム時の描画レスポンスについては良いようです。

[^1]: ただし 1億点をプロットしようとすると、配列管理の上限を超えるようで、異常発生しました。

<img width="260" alt="scatter.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54617/11ee48b2-4da1-1f2b-19aa-fa584503157c.jpeg">

```scatter.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset=utf-8>
    <style>
    html,body{
        height: 100%;
        margin: 0;
    }
    .canvas{
        height: 100%;
        padding: 0px;
    }
    </style>
  </head>
  <body>
    <div class="canvas"></div>
  </body>
  <script language="javascript" src="https://npmcdn.com/regl/dist/regl.min.js"></script>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script language="javascript">
  const pointSize = 8;
  const nPoints =   100000;
  const timerDelay = 100;
  const deltaY = 100;

  const rng = d3.randomNormal(0, 0.3);
  const points = d3.range(nPoints).map(() => 
     ({ x: rng(),
        y: rng(),
        color: [Math.random(), Math.random(), 0]})
  );

  let scale = 1, offset = [0, 0];
  let timer, dragStart;

  const mouseDown = e => {
    dragStart = {x: e.pageX, y: e.pageY};
  }

  const mouseUp = e => {
    offset[0] += (e.pageX - dragStart.x) * 2 / window.innerWidth;
    offset[1] += (e.pageY - dragStart.y) * 2 / window.innerHeight;
    draw();
  }

  const wheelScroll = e => {
    const magnification = Math.exp(-e.deltaY / deltaY);
    scale *= magnification;
    offset[0] *= magnification;
    offset[1] *= magnification;
    draw();
  }

  const resize = e => {
    if (timer !== false) {
      clearTimeout(timer);
    }
    timer = setTimeout(function () {
      regl.poll();
      draw();
    }, timerDelay);
  };

  document.addEventListener("mousedown", mouseDown, false);
  document.addEventListener("mouseup", mouseUp, false);
  document.addEventListener("wheel", wheelScroll);
  window.addEventListener('resize', resize);

const frag = `
  precision highp float;
  varying vec3 fragColor;
  void main() {
    gl_FragColor = vec4(fragColor, 1);
  }`;

const vert = `
  varying vec3 fragColor;
  attribute vec2 pos;
  attribute vec3 color;
  uniform float pointSize;
  uniform float scale;
  uniform vec2 offset;

  vec2 normalizeCoords(vec2 pos) {
    float x = pos[0] * scale + offset[0];
    float y = pos[1] * scale + offset[1];
    return vec2(x, -y);
  }

  void main() {
    fragColor = color;
    gl_PointSize = pointSize;
    gl_Position = vec4(normalizeCoords(pos), 0, 1);
  }`;


  const draw = () => {
    drawPoints({scale: scale, offset: offset});
  }

  const regl = createREGL({
    container: document.querySelector('.canvas'),
  });

  const drawPoints = regl({
    frag: frag,
    vert: vert,
    primitive: 'points',
    count: points.length,
    attributes: {
      pos: points.map(d => [d.x, d.y]),
      color: points.map(d => d.color)
    },
    uniforms: {
      pointSize: pointSize,
      scale: regl.prop('scale'),
      offset: regl.prop('offset'),
    }
  });

  draw();
  </script>
</html>
```