block by micahstubbs e97e688f33d41f247700376c95b62cce

regl-animate-circles

Full Screen

an experiment that swaps the color source chroma.brewer.YlGnBu for d3.interpolateMagma(t)

an iteration on the block ReGL circle animation example from @ajdant

commit history and related experiments here

index.html

<html>
<body>
<script src='https://cdnjs.cloudflare.com/ajax/libs/regl/1.3.0/regl.min.js'></script>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script src='vis.js'></script>
</body>
</html>

lebab.sh

# safe
lebab --replace vis.js --transform arrow
lebab --replace vis.js --transform for-of
lebab --replace vis.js --transform for-each
lebab --replace vis.js --transform arg-rest
lebab --replace vis.js --transform arg-spread
lebab --replace vis.js --transform obj-method
lebab --replace vis.js --transform obj-shorthand
lebab --replace vis.js --transform multi-var
# unsafe
lebab --replace vis.js --transform let
lebab --replace vis.js --transform template

vis.js

/* global createREGL document window */

const regl = createREGL();

const doc = document.body;
const docWidth = window.innerWidth;
const docHeight = window.innerHeight;
const aspectRatio = docHeight / docWidth;

const colCount = 150;
const colWidth = docWidth / colCount;

const rowCount = parseInt(colCount * aspectRatio, 10);
const rowHeight = docHeight / rowCount;

const circleCount = rowCount * colCount;
const maxRadius = Math.min(colWidth, rowHeight) / 2;

const minScale = 1e-6; // use small number to avoid reaching zero
const maxScale = 1.0;
const randomScale = () => minScale + ((maxScale - minScale) * Math.random());

// sample nine colors from the magma color space
const palette = [0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0].map(t => d3.interpolateMagma(t));
console.log('palette', palette);

// gl mode uses [r,g,b] vector w normalised values
const paletteGL = palette.map(d => {
  const cGL = d3.color(d).rgb();
  return [cGL.r, cGL.g, cGL.b].map(e => e / 255); 
}); 
console.log('paletteGL', paletteGL);

const randomColor = () => paletteGL[Math.floor(palette.length * Math.random())];

const getGridPoints = (cols, rows, colWidth, rowHeight, gridWidth, gridHeight) => {
  const count = cols * rows;
  const points = new Float32Array(count * 2);

  let col;
  let row;
  let xPos;
  let yPos;
  let xIndex;
  let yIndex;

  for (let i = 0; i < count; i += 1) {
    col = i % cols;
    row = parseInt(i / cols, 10);

    xPos = (col * colWidth) + (colWidth / 2);
    yPos = (row * rowHeight) + (rowHeight / 2);

    xIndex = (2 * i);
    yIndex = xIndex + 1;

    points[xIndex] = ((2 * xPos) / gridWidth) - 1;  // convert to (-1, 1) GL coord space
    points[yIndex] = ((2 * yPos) / gridHeight) - 1; // convert to (-1, 1) GL coord space
  }

  return points;
};

const getAnimStates = (count) => {
  const colors = new Float32Array(count * 3);
  const scales = new Float32Array(count);

  let r;
  let g;
  let b;
  let rIndex;
  let gIndex;
  let bIndex;

  for (let i = 0; i < count; i += 1) {
    [r, g, b] = randomColor();

    rIndex = (3 * i);
    gIndex = rIndex + 1;
    bIndex = rIndex + 2;

    colors[rIndex] = r;
    colors[gIndex] = g;
    colors[bIndex] = b;

    scales[i] = randomScale();
  }

  return { colors, scales };
};

// build inputs for our animation:
const centroids = getGridPoints(colCount, rowCount, colWidth, rowHeight, docWidth, docHeight);

// create a series of animation states for each circle
const numStates = 50;
const allStates = [];

while (allStates.length < numStates) {
  allStates.push(getAnimStates(circleCount));
}

// regl command featuring shaders that will draw supplied points
const drawPoints = regl({
  vert: `
  precision highp float;

  uniform float progress; // interpolation progress (from 0.0 to 1.0)
  uniform float maxRadius; // max radius to draw (n.b. point is square of 2*r x 2*r)

  attribute vec2 point; // position at which to draw

  attribute float scaleA; // scale factor A
  attribute float scaleB; // scale factor B

  attribute vec3 colorA; // color A
  attribute vec3 colorB; // color B

  varying vec3 rgb; // interpolated color
  varying float scale; // interpolated scale

  void main () {
    rgb = mix(colorA, colorB, progress);
    scale = mix(scaleA, scaleB, progress);

    gl_PointSize = maxRadius * 2. * scale;
    gl_Position = vec4(point, 0, 1);
  }
  `,

  frag: `
  precision highp float;

  varying vec3 rgb;
  varying float scale;

  void main () {
    // determine normalized distance from center of point
    float point_dist = length(gl_PointCoord * 2. - 1.);

    // calc scale at which to start fading out the circle
    float min_dist = scale * 0.70;

    // calc scale at which we find the edge of the circle
    float max_dist = scale;

    // https://thebookofshaders.com/glossary/?search=smoothstep
    float alpha = 1. - smoothstep(min_dist, max_dist, point_dist);

    gl_FragColor = vec4(rgb, alpha);
  }
  `,

  // using textbook example from http://regl.party/api#blending
  blend: {
    enable: true,
    func: {
      srcRGB: 'src alpha',
      srcAlpha: 1,
      dstRGB: 'one minus src alpha',
      dstAlpha: 1,
    },
    equation: {
      rgb: 'add',
      alpha: 'add',
    },
    color: [0, 0, 0, 0],
  },

  attributes: {
    point: centroids,
    colorA: regl.prop('currColors'),
    colorB: regl.prop('nextColors'),
    scaleA: regl.prop('currScales'),
    scaleB: regl.prop('nextScales'),
  },

  uniforms: {
    maxRadius,
    progress: regl.prop('progress'),
  },

  count: circleCount,

  primitive: 'points',

});

// render loop
const fps = 60;
const tweenTime = 2;
const tweenFrames = fps * tweenTime;

let state = 0;

regl.frame(({ tick }) => {
  regl.clear({
    color: [0, 0, 0, 1],
    depth: 1,
  });

  // increment frame counter until we reach the desired loop point
  const frame = tick % tweenFrames;

  // increment state counter once we've looped back around
  if (frame === 0) {
    state = (state + 1) % numStates;
    // console.log('state counter', state);
  }

  // track progress as proportion of frames completed
  const progress = frame / tweenFrames;

  // determine current and next state
  const currState = allStates[state];
  const nextState = allStates[(state + 1) % numStates];

  drawPoints({
    currColors: currState.colors,
    currScales: currState.scales,
    nextColors: nextState.colors,
    nextScales: nextState.scales,
    progress,
  });
});