function createDrawPoints(points) {
const drawPoints = regl({
frag: `
// set the precision of floating point numbers
precision highp float;
// this value is populated by the vertex shader
varying vec3 fragColor;
void main() {
// gl_FragColor is a special variable that holds the color of a pixel
gl_FragColor = vec4(fragColor, 1);
}
`,
vert: `
// per vertex attributes
attribute vec2 positionStart;
attribute vec2 positionEnd;
attribute vec3 colorStart;
attribute vec3 colorEnd;
// variables to send to the fragment shader
varying vec3 fragColor;
// values that are the same for all vertices
uniform float pointWidth;
uniform float stageWidth;
uniform float stageHeight;
uniform float elapsed;
uniform float duration;
// helper function to transform from pixel space to normalized device
// coordinates (NDC)
// in NDC (0,0) is the middle, (-1, 1) is the top left and (1, -1) is
// the bottom right.
vec2 normalizeCoords(vec2 position) {
// read in the positions into x and y vars
float x = position[0];
float y = position[1];
return vec2(
2.0 * ((x / stageWidth) - 0.5),
// invert y since we think [0,0] is bottom left in pixel space
-(2.0 * ((y / stageHeight) - 0.5)));
}
// helper function to handle cubic easing (copied from d3 for consistency)
// note there are pre-made easing functions available via glslify.
float easeCubicInOut(float t) {
t *= 2.0;
t = (t <= 1.0 ? t * t * t : (t -= 2.0) * t * t + 2.0) / 2.0;
if (t > 1.0) {
t = 1.0;
}
return t;
}
void main() {
// update the size of a point based on the prop pointWidth
gl_PointSize = pointWidth;
// number between 0 and 1 indicating how far through the animation this
// vertex is.
float t;
// drawing without animation, so show end state immediately
if (duration == 0.0) {
t = 1.0;
// otherwise we are animating, so use cubic easing
} else {
t = easeCubicInOut(elapsed / duration);
}
// interpolate position
vec2 position = mix(positionStart, positionEnd, t);
// interpolate and send color to the fragment shader
fragColor = mix(colorStart, colorEnd, t);
// scale to normalized device coordinates
// gl_Position is a special variable that holds the position of a vertex
gl_Position = vec4(normalizeCoords(position), 0.0, 1.0);
}
`,
attributes: {
// each of these gets mapped to a single entry for each of the points.
// this means the vertex shader will receive just the relevant value for
// a given point.
positionStart: points.map(d => [d.sx, d.sy]),
positionEnd: points.map(d => [d.tx, d.ty]),
colorStart: points.map(d => d.colorStart),
colorEnd: points.map(d => d.colorEnd),
},
uniforms: {
// by using `regl.prop` to pass these in, we can specify them as arguments
// to our drawPoints function
pointWidth: regl.prop('pointWidth'),
// regl actually provides these as viewportWidth and viewportHeight but I
// am using these outside and I want to ensure they are the same numbers,
// so I am explicitly passing them in.
stageWidth: regl.prop('stageWidth'),
stageHeight: regl.prop('stageHeight'),
duration: regl.prop('duration'),
// time in milliseconds since the prop startTime (i.e. time elapsed)
// note that `time` is passed by regl whereas `startTime` is a prop passed
// to the drawPoints function.
elapsed: ({ time }, { startTime = 0 }) => (time - startTime) * 1000,
},
// specify the number of points to draw
count: points.length,
// specify that each vertex is a point (not part of a mesh)
primitive: 'points',
});
return drawPoints;
}