Code from codepen.io/monfera/pen/GjOBkJ
Update: now it uses the WebGL extension OES_standard_derivatives
for antialiasing, if avaliable. There’s also Z ordering and alpha blending, to support antialiasing with the alpha smoothstepping method. There’s a slight trick, fwidth
is applied on dist
rather than r
so as to avoid artifacts where the formula yields near-zero radii.
It shows:
vec4
position attribute) to the GPU, and calculating the tweening on the GPU (contrast this to refreshing geometry in a rAF loopnumpy
-like interface, pretending that native typed JS arrays are multidimensional arrays (syntactic sugar in this case)It’s not meant to be efficient as it’s more of a test for various features. It can be made much faster/nicer in various ways.
Links:
regl
and much of scijs
scijs/ndarray
contributorregl
Uploaded with blockbuilder.org
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://rawcdn.githack.com/monfera/ndarray-bundle/cbbafcb5/ndarray.min.js"></script>
<script src="https://rawcdn.githack.com/regl-project/regl/v1.3.13/dist/regl.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<script>
// //colorbrewer2.org/#type=qualitative&scheme=Paired&n=12
// except yellow
const palette = [
[166,206,227],
[ 31,120,180],
[178,223,138],
[ 51,160, 44],
[251,154,153],
[227, 26, 28],
[253,191,111],
[255,127, 0],
[202,178,214],
[106, 61,154],
[177, 89, 40]
]
const regl = window.createREGL({extensions: ['OES_standard_derivatives']})
const lineCount = 11
const pointCount = 5
const lineWidth = 2
// padding is interpreted as percentage of the screen width or height
const xPadding = 0.1
const yPadding = 0.25
const xScale = x => 2 * (1 - 2 * xPadding) * (x - 0.5)
const yScale = y => 2 * (1 - 2 * yPadding) * (y - 0.5)
// nd :: typedArrayClass -> array -> ndarray
const nd = (Array, dimensions) => ndarray(
new Array(dimensions.reduce((p, n) => p * n, 1)),
dimensions
)
const colorLineAesthetic = ({lineWidth}) => ({
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]
},
depth: {
enable: true,
mask: true,
func: 'less',
range: [0, 1]
},
vert: `
precision mediump float;
attribute vec3 positionFrom;
attribute vec3 positionTo;
attribute vec4 color;
uniform float tween;
varying vec4 c;
float smootherstep(float edge0, float edge1, float x) {
x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
}
void main() {
vec3 tweened = mix(positionFrom, positionTo, smootherstep(0.0, 1.0, tween));
gl_Position = vec4(tweened, 1.0);
c = color;
gl_PointSize = 60.0;
}`,
frag: `
#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives : enable
#endif
precision mediump float;
uniform mat3 S1;
uniform mat3 S2;
uniform float tween;
varying vec4 c;
float superFormula(mat3 S, float fi) {
float a = S[0][0];
float b = S[0][1];
float m1 = S[0][2];
float m2 = S[1][0];
float n1 = S[1][1];
float n2 = S[1][2];
float n3 = S[2][0];
float s = S[2][1];
float r = pow( pow(abs(cos(m1 * fi / 4.0) / a), n2)
+ pow(abs(sin(m2 * fi / 4.0) / b), n3),
-1.0 / n1
);
return s * r;
}
void main() {
float alpha = 1.0, delta = 0.0;
vec2 pxy = 2.0 * gl_PointCoord.xy - 1.0;
float dist = length(pxy);
float fi = atan(pxy.y, pxy.x);
float r1 = superFormula(S1, fi);
float r2 = superFormula(S2, fi);
float r = mix(r1, r2, smoothstep(0.0, 1.0, tween));
float R = dist - r + 1.0;
#ifdef GL_OES_standard_derivatives
delta = fwidth(dist);
if(R > 1.0 + delta ) discard; // avoid further calc, blending if possible
alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, R);
#else
if(R > 1.0) discard;
#endif
gl_FragColor = vec4(c.xyz, c.a * alpha);
}`,
primitive: 'points',
lineWidth
})
/* For performance reasons, the model format is identical with the format
* that regl expects attribute arrays to be in.
*/
const model = (({lineCount, pointCount, xScale, yScale}) => {
const positionLength = 3
const colorLength = 4
const positionFrom = nd(Float32Array, [lineCount, pointCount, positionLength])
const positionTo = nd(Float32Array, [lineCount, pointCount, positionLength])
const color = nd(Float32Array, [lineCount, pointCount, colorLength])
let i, j, x1, x2, y1, y2, z
for(i = 0; i < lineCount; i++) {
for(j = 0; j < pointCount; j++) {
x1 = Math.random()
x2 = Math.random()
y1 = Math.random()
y2 = Math.random()
z = 1 - (j + i * pointCount) / (lineCount * pointCount)
positionFrom.set(i, j, 0, xScale(x1))
positionFrom.set(i, j, 1, yScale(y1))
positionFrom.set(i, j, 2, z)
positionTo.set(i, j, 0, xScale(x2))
positionTo.set(i, j, 1, yScale(y2))
positionTo.set(i, j, 2, z)
color.set(i, j, 0, palette[i][0] / 255)
color.set(i, j, 1, palette[i][1] / 255)
color.set(i, j, 2, palette[i][2] / 255)
color.set(i, j, 3, 0.8)
}
}
return {
lineCount,
pointCount,
attributes: {
positionFrom: positionFrom.data,
positionTo: positionTo.data,
color: color.data
},
staticUniforms: {
// cross
S1 : [
/* a= */ 1,
/* b= */ 0.6875,
/* m1= */ 8,
/* m2= */ 8,
/* n1= */ 1.3,
/* n2= */ 0.01,
/* n3= */ 3.313,
/* s= */ 0.99, // leave room for antialiasing band
/* */ 0,
],
// hexagon
S2 : [
/* a= */ 1,
/* b= */ 1,
/* m1= */ 6,
/* m2= */ 6,
/* n1= */ 100,
/* n2= */ 40,
/* n3= */ 40,
/* s= */ 0.8, // make hexagon smaller for similar visual weight
/* */ 0,
]
}
}
})({lineCount, pointCount, xScale, yScale})
const lineViewModelMaker = model => {
const elements = []
let i, j
let index = 0
const {lineCount, pointCount, attributes, staticUniforms} = model
const uniforms = Object.assign(
{},
staticUniforms,
{
tween: ({time}) => Math.cos(time) + 1 / 2
})
for(i = 0; i < lineCount; i++) {
for(j = 0; j < pointCount; j++) {
elements.push(index)
index++
}
}
return {attributes, uniforms, elements }
}
const linesDrawerMaker = model => {
const aesthetic = colorLineAesthetic
const layer = Object.assign(
{},
lineViewModelMaker(model),
aesthetic({lineWidth: lineWidth})
)
return regl(layer)
}
const linesDrawer = linesDrawerMaker(model)
const render = () => {
regl.frame(() => {
regl.clear({
color: [1, 1, 1, 1]
})
linesDrawer()
})
}
render()
</script>
</body>