Published
Edited
Mar 22, 2021
Insert cell
Insert cell
Insert cell
Insert cell
canvas = html`<canvas width="960" height="600">`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// duration of the animation ignoring delays
duration = 1500; // 1500ms = 1.5s
Insert cell
// create initial set of points
// initialize with all the points in the middle of the screen and black
points = d3.range(numPoints).map(i => ({
tx: width / 2,
ty: height / 2,
colorEnd: [0, 0, 0],
}))
Insert cell
// helper to layout points in a green fuzzy circle
function greenCircleLayout(points) {
const rng = d3.randomNormal(0, 0.05);
points.forEach((d, i) => {
d.x = (rng() + Math.cos(i)) * (width / 2.5) + width / 2;
d.y = (rng() + Math.sin(i)) * (height / 2.5) + height / 2;
d.color = [0, Math.random(), 0]; // random amount of green
});
}
Insert cell
// helper to layout points in a normally distributed area, colored blue
function blueNormalLayout(points) {
const rng = d3.randomNormal(0, 0.15);
points.forEach(d => {
d.x = rng() * width + width / 2;
d.y = rng() * height + height / 2;
d.color = [0, d.color[1] * 0.5, 0.9]; // some previous green and 0.9 blue
});
}
Insert cell
// set the order of the layouts and some initial animation state
layouts = [greenCircleLayout, blueNormalLayout];
Insert cell
layoutStats = ({
currentLayout: 0 // start with green circle layout
})
Insert cell
// function to compile a draw points regl func
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;
}
Insert cell
// function to start the animation loop (note: time is in seconds)
function animate(layout, points) {
console.log('animating with new layout');
// make previous end the new beginning
points.forEach(d => {
d.sx = d.tx;
d.sy = d.ty;
d.colorStart = d.colorEnd;
});

// layout points
layout(points);

// copy layout x y to end positions
points.forEach((d, i) => {
d.tx = d.x;
d.ty = d.y;
d.colorEnd = d.color;
});

// create the regl function with the new start and end points
const drawPoints = createDrawPoints(points);

// start an animation loop
let startTime = null; // in seconds
const frameLoop = regl.frame(({ time }) => {
// keep track of start time so we can get time elapsed
// this is important since time doesn't reset when starting new animations
if (startTime === null) {
startTime = time;
}
// clear the buffer
regl.clear({
// background color (white)
color: [1, 1, 1, 1],
depth: 1,
});

// draw the points using our created regl func
// note that the arguments are available via `regl.prop`.
drawPoints({
pointWidth,
stageWidth: width,
stageHeight: height,
duration,
startTime,
});

// if we have exceeded the maximum duration, move on to the next animation
if (time - startTime > duration / 1000) {
console.log('done animating, moving to next layout');

// cancel this loop, we are going to start another
frameLoop.cancel();

// increment to use next layout function
layoutStats.currentLayout = (layoutStats.currentLayout + 1) % layouts.length;

// start a new animation loop with next layout function
animate(layouts[layoutStats.currentLayout], points);
}
});
}
Insert cell
// start the initial animation
animate(layouts[layoutStats.currentLayout], points);
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more