Built with blockbuilder.org
An animated version of this generative art pop up on my twitter feed and it caught my eye. The original was created in Processing and each frame saved to a PNG file, so I decided to create a browser-based version in d3.
I started with a straightforward d3 and canvas port, to understand what the code was doing. With ~24k points, this version had a poor framerate and wasn't satisfying to watch. The WebGL port, on the other hand, proved much more challanging (this was my first attempt at using WebGL) but with very pleasing results.
A few helpful links I gathered along the way:
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script language="javascript" src="https://npmcdn.com/regl/dist/regl.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-random.v1.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<div id="art"></div>
<script type="text/javascript">
// based on https://deconbatch.blogspot.com/2017/12/interstellar-overdrive.html
const numPoints = 360*68;
var width = isNaN(window.innerWidth) ? window.clientWidth : window.innerWidth,
height = isNaN(window.innerHeight) ? window.clientHeight : window.innerHeight;
const pixelRatio = width/height;
const sizeBase = Math.max(width,height) / 64.;
if (isNaN(width) || width==0) {
width = 960; height=500;
}
var c = d3.select('#art').append("canvas")
.attr('width', width)
.attr('height', height);
var canvas = c.node();
function main(err, regl) {
var scaleDivBase = d3.scaleLinear().domain([0.0, 1.0]).range([0.00005, 0.00025]);
var scale360 = d3.scaleLinear().domain([0.0, 1.0]).range([0, 360.0]);
var scaleRadius = d3.scaleLinear().range([1.0, 1.6]).domain([-2.0, 2.0]);
var scaleXY = d3.scaleLinear().domain([-1.0, 1.0]);
var rnd = d3.randomUniform(1.0);
// pick a random color
const colorBase = d3.randomUniform(360.0)();
const col = d3.color('hsl(' + colorBase + ', 60%, 40%)').rgb();
const colorArray = [col.r/256., col.g/256., col.b/256.];
var cxnoiseDivBase = scaleDivBase(rnd());
var cynoiseBase = scale360(rnd());
var csnoiseBase = scale360(rnd());
function radians(degrees) {
return degrees * Math.PI / 180.0;
}
// function to compile a draw points regl func
function createDrawPoints(points) {
const drawPoints = regl({
frag: `
#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives : enable
#endif
// set the precision of floating point numbers
precision mediump float;
// these values are populated by the vertex shader
varying vec3 fragColor;
varying float fragAlpha;
void main() {
// make our points look like circles
float r = 0.0, delta = 0.0, alpha = 1.0;
vec2 cxy = 2.0 * gl_PointCoord - 1.0;
r = dot(cxy, cxy);
// soften the edges a bit
#ifdef GL_OES_standard_derivatives
delta = fwidth(r);
alpha = 0.9 - smoothstep(1.0 - delta, 1.0 + delta, r);
#endif
if (r > 1.0) {
discard;
}
gl_FragColor = vec4(fragColor, fragAlpha * alpha);
}
`,
vert: `
// per vertex attributes
attribute vec2 position;
attribute float idx;
// variables to send to the fragment shader
varying vec3 fragColor;
varying float fragAlpha;
// values that are the same for all vertices
uniform float stageWidth;
uniform float stageHeight;
uniform float t; //time
uniform vec3 noiseBase;
uniform float sizeBase;
uniform float numPoints;
uniform vec3 color;
// 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, float rotation) {
// compute rotation matrix
float pixelRatio = stageHeight / stageWidth;
float c = cos(rotation);
float s = sin(rotation);
mat2 r = mat2(c, -s * pixelRatio, s, c * pixelRatio);
// read in the positions into x and y vars
float x = position[0];
float y = position[1];
// norm to device coords
vec2 v = 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)));
// center
v = v + vec2(1., -1.);
// apply rotation
return v * r;
}
// noise helper function
vec3 customNoise(vec3 value) {
vec3 s = sin(value);
return (s*s*s) * cos(value*value);
}
// helper function that is the equivilant of processing's map function
float map(float value, float istart, float istop, float ostart,float ostop) {
return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
}
// helper function that applies map to both elements of a vector
vec2 map(vec2 value, float istart, float istop, float ostart,float ostop) {
return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
}
void main() {
// compute noise-vector @ time t = x, y, s
vec3 custNoise = customNoise(noiseBase + vec3(
(idx) * t*0.00000001, //wave
(1.+idx)*0.01 + t*0.0000015, //roll
(1.+idx)*0.005 + t*0.000005 //blink
));
// calculate x/y coords based on the noise function
float radius = (15.0 + (idx * 0.02)) * map(custNoise[0] + custNoise[1], -2.0, 2.0, 1.0, 1.6);
vec2 ePt = map(position, -1., 1., -radius, radius);
// send color and alpha to the fragment shader
fragColor = color;
fragAlpha = 1. - abs(custNoise[2]) + 0.1;
// set position and point size
gl_Position = vec4(normalizeCoords(ePt, radians(t/200.)), 0, 1.0);
gl_PointSize = (2.0 + abs(custNoise[2]) * sizeBase) * map(idx, 0.0, numPoints, 0.15, 1.5);
}
`,
attributes: {
position: points.map(d => [Math.cos(d.rads), Math.sin(d.rads)]),
idx: points.map(d => d.idx)
},
uniforms: {
stageWidth: regl.prop('stageWidth'),
stageHeight: regl.prop('stageHeight'),
color: regl.prop('color'),
noiseBase: regl.prop('noiseBase'),
sizeBase: regl.prop('sizeBase'),
numPoints: regl.prop('numPoints'),
t: ({ time }) => time * 1000,
},
blend: {
enable: true,
func: { srcRGB: 'src alpha', dstRGB: 'one minus src alpha', srcAlpha: 1, dstAlpha: 'one minus src alpha' }, //1
},
// 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;
}
// function to start the animation loop
function animate(points) {
// setup the regl program
const drawPoints = createDrawPoints(points);
// start an animation loop
const frameLoop = regl.frame(({ time }) => {
regl.clear({color: [1.,1.,1.,1.], depth: 1});
// draw the points using our created regl func
drawPoints({
stageWidth: width,
stageHeight: height,
noiseBase: [cxnoiseDivBase, cynoiseBase, csnoiseBase],
color: colorArray,
sizeBase: sizeBase,
numPoints: numPoints
});
});
}
// create initial set of points
const points = d3.range(numPoints).map(i => ({
rads: radians(i*0.1),
idx: i
}));
// start the initial animation
animate(points);
}
var regl = createREGL({
extensions: ['OES_standard_derivatives'],
canvas: canvas,
attributes: {
alpha: false,
depth: false,
antialias: true
},
onDone: main
});
</script>
</body>
https://npmcdn.com/regl/dist/regl.js
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-random.v1.min.js