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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more