Code
const pixelRatio = window.devicePixelRatio;
const regl = createREGL({
pixelRatio,
extensions: [
'ANGLE_instanced_arrays',
'OES_standard_derivatives',
],
attributes: {
antialias: true
}
});
regl._gl.canvas.style.position = 'fixed';
function createNumberCanvas (size) {
const canvas = document.createElement('canvas');
const img = document.createElement('img');
const ctx = canvas.getContext('2d');
canvas.width = 10 * size;
canvas.height = size;
ctx.font = `${size}px monospace`;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, size * 10, size);
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 0; i < 10; i++) {
ctx.fillText(i, (i + 0.5) * size, size * 0.5);
}
const returnValue = new Promise(resolve => {
img.onload = () => resolve(img);
});
img.src = canvas.toDataURL();
return returnValue;
}
const createREGLProxy = function (regl, argWrapper) {
const proxy = args => regl({...args, ...argWrapper(args)});
Object.assign(proxy, regl);
return proxy;
}
const reglProxy = createREGLProxy(regl, function (args) {
if (!args.vert) return {};
return {
vert: args.vert.slice(0, args.vert.length - 1) + `
float theta = (2.0 * pi) * (0.5 + index / 8.428);
gl_Position.xy += vec2(cos(theta), sin(theta)) * numberOffset * pixelRatio / _resolution;
}`
}
});
const state = wrapGUI(State({
lineConfig: State.Section({
capResolution: State.Slider(8, {min: 1, max: 32, step: 1}),
joinResolution: State.Slider(2, {min: 1, max: 32, step: 1}),
cap: State.Select('round', {options: ['round', 'square', 'none']}),
join: State.Select('round', {options: ['round', 'miter', 'bevel']}),
miterLimit: State.Slider(1.7, {min: 1, max: 8, step: 0.01}),
insertCaps: true
}, {label: 'line config', expanded: true}),
geometry: State.Section({
stretch: State.Slider(0.9, {min: -2, max: 2, step: 0.001}),
flip: State.Slider(1.0, {min: -1, max: 1, step: 0.001}),
}, {expanded: true}),
line: State.Section({
width: State.Slider(70, {min: 1, max: 100, step: 0.1}),
opacity: State.Slider(0.8, {min: 0, max: 1, step: 0.01}),
}, {label: 'line', expanded: false}),
border: State.Section({
width: State.Slider(5, {min: 0, max: 10, step: 0.1}),
opacity: State.Slider(0.65, {min: 0, max: 1, step: 0.01}),
}, {expanded: false}),
dash: State.Section({
length: State.Slider(0.2, {min: 0, max: 2, step: 0.05}),
opacity: State.Slider(0.3, {min: 0, max: 1, step: 0.01}),
}, {expanded: false, label: 'dash'}),
rendering: State.Section({
wireframeOpacity: State.Slider(0.5, {min: 0, max: 1, step: 0.01}),
cull: State.Select('none', {options: ['none', 'front', 'back']}),
depth: false,
colorInstances: true,
labelPoints: false
}, {
expanded: false
})
}), {
containerCSS: `
position: absolute;
right: 0;
`
});
state.$onChange(draw);
function project(p) {
return [
(0.5 + 0.5 * (p[0] * Math.pow(state.geometry.stretch, 4.0) * Math.sign(state.geometry.stretch) - 0.2)) * window.innerWidth,
(0.5 + 0.5 * (p[1] * Math.pow(state.geometry.flip, 4.0) * Math.sign(state.geometry.flip))) * window.innerHeight,
];
}
const path = [
//[-Infinity, -Infinity],
[-0.9, 0.1],
[-0.8, -0.3],
[-0.7, -0.4],
[-0.5, -0.5],
[-0.4, -0.3],
[-Infinity, -Infinity],
[-0.3, -0.1],
[-0.2, 0.1],
[-Infinity, -Infinity],
[-0.1, 0.3],
[ 0, 0.5],
[ 0.3, 0.4],
[ 0.4, -0.5],
[ 0.7, -0.5],
[ 0.9, 0.5],
//[-Infinity, -Infinity],
];
const dist = Array(path.length).fill(0);
function computeCumulativeDistance (dist, path, project) {
let prevPoint = project(path[0]);
for (let i = 1; i < path.length; i++) {
dist[i] = dist[i - 1];
const point = project(path[i]);
const l = Math.hypot(point[0] - prevPoint[0], point[1] - prevPoint[1]);
if (!isNaN(l) && isFinite(l)) dist[i] += l;
prevPoint = point;
}
return dist;
}
const lineData = {
vertexCount: path.length,
vertexAttributes: {
point: regl.buffer(path),
dist: regl.buffer(dist),
},
endpointCount: 2,
endpointAttributes: {
point: regl.buffer([path.slice(0, 3), path.slice(-3).reverse()]),
dist: regl.buffer([dist.slice(0, 3), dist.slice(-3).reverse()]),
},
};
let numTex = regl.texture([[0, 0, 0, 0]]);
createNumberCanvas(30).then(img => {
numTex = numTex({data: img, min: 'linear', mag: 'linear'})
draw();
});
const commandCache = {};
function getDrawLines(config) {
config = Object.assign({}, {
primitive: 'triangle strip',
}, config);
const {primitive} = config;
const cacheKey = JSON.stringify(config);
if (!commandCache[cacheKey]) {
commandCache[cacheKey] = reglLines(reglProxy, {
debug: true,
vert: `
precision highp float;
#pragma lines: attribute vec2 point;
#pragma lines: attribute float dist;
#pragma lines: position = project(point);
#pragma lines: width = getWidth();
#pragma lines: extrapolate varying float dist = getProgress(dist);
uniform float stretch, flip, lineWidth, borderWidth, numberOffset;
uniform float pixelRatio;
float getProgress(float p) { return p; }
float getPointIndex(float p) { return p; }
vec4 project (vec2 p) {
if (p.x <= -1.0) return vec4(0);
return vec4(p * vec2(pow(abs(stretch), 4.0) * sign(stretch), pow(abs(flip), 4.0) * sign(flip)), 0, 1) - vec4(0.2,0,0,0);
}
float getWidth () {
gl_PointSize = 10.0 * pixelRatio; // Oops; has to be anywhere within main func
return lineWidth;
}`,
frag: `
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform bool squareCap, useBorder, colorInstances;
uniform float pixelRatio, dashLength, lineWidth, borderWidth, wireframeOpacity;
uniform vec4 borderColor, lineColor, dashColor;
varying float vertexIndex;
uniform sampler2D numTex;
varying vec3 lineCoord;
varying float dist;
varying float instanceID;
varying vec2 triStripCoord;
varying float dir;
float grid (vec3 parameter, float width, float feather) {
float w1 = width - feather * 0.5;
vec3 d = fwidth(parameter);
vec3 looped = 0.5 - abs(mod(parameter, 1.0) - 0.5);
vec3 a3 = smoothstep(d * (w1 + feather), d * w1, looped);
return max(max(a3.x, a3.y), a3.z);
}
float linearstep (float a, float b, float x) {
return clamp((x - a) / (b - a), 0.0, 1.0);
}
void main () {
float sdf = lineWidth * 0.5 * (
squareCap ? max(abs(lineCoord.x), abs(lineCoord.y)) : length(lineCoord.xy)
);
gl_FragColor.a = lineColor.a;
gl_FragColor.rgb = vec3(0.4, 0.7, 1.0);
if (colorInstances) {
if (instanceID < 0.0) {
gl_FragColor.rgb = vec3(0.8, 0.1, 0.4);
} else if (floor(mod(instanceID, 2.0) + 0.5) == 1.0) {
gl_FragColor.rgb = vec3(0.2, 0.3, 0.7);
}
} else {
gl_FragColor.rgb = lineColor.rgb;
}
#if ${primitive === 'triangle strip' ? 0 : 1}
bool neg = vertexIndex < 0.0;
vec2 uv = gl_PointCoord.xy;
float ones = mod(floor(abs(vertexIndex) + 0.5), 10.0);
float tens = floor((abs(vertexIndex) + 0.5) / 10.0);
if (tens == 0.0) uv.x += 0.25;
if (tens == 0.0 && uv.x < 0.5 || uv.x > 1.0) discard;
vec2 numCenter = vec2(((uv.x < 0.5 ? tens : ones) + 0.5) / 10.0, 0.5);
vec2 numRange = vec2(0.5 / 10.0, 0.5);
vec2 numCoord = vec2(fract(uv.x * 2.0) - 0.5, uv.y * 2.0 - 1.0);
gl_FragColor = vec4(
mix(gl_FragColor.rgb, vec3(1), 0.8),
texture2D(numTex, numCenter + numRange * numCoord).r
);
#else
float dl = dashLength;
if (dashColor.a > 0.0 && dashLength > 0.0) {
float dashvar = fract(dist / dl) * dl;
float dash = linearstep(0.0, 1.0, dashvar)
* linearstep(dl * 0.5 + 1.0 / pixelRatio, dl * 0.5, dashvar);
//if (lineCoord.z > 0.0) dash = 0.0;
gl_FragColor.a *= mix(1.0, 1.0 - dashColor.a, dash);
}
if (useBorder && borderColor.a > 0.0) {
float border = linearstep(
lineWidth * 0.5 - borderWidth - 0.5,
lineWidth * 0.5 - borderWidth + 0.5,
sdf
);
vec3 borderCol = lineCoord.y > 0.0 ? vec3(1, 0, 0) : vec3(0,0,1);
gl_FragColor.rgb = mix(gl_FragColor.rgb, borderCol, border * borderColor.a);
gl_FragColor.a = max(gl_FragColor.a, borderColor.a * border);
}
// Draw unit grid lines and a diagonal line using the vertex ID turned into a vec2 varying.
//
// 0 2 4 6 8
// + --- + --- + --- + --- +
// | / | / | / | / |
// | / | / | / | / |
// + --- + --- + --- + --- +
// 1 3 5 7 9
//
if (wireframeOpacity > 0.0) {
float wire = grid(vec3(triStripCoord, triStripCoord.x + triStripCoord.y), 0.5 * pixelRatio, 2.0 / pixelRatio);
gl_FragColor = mix(gl_FragColor, vec4(1), wire * wireframeOpacity);
}
#endif
}`,
uniforms: {
numTex,
colorInstances: regl.prop('rendering.colorInstances'),
numberOffset: regl.prop('numberOffset'),
wireframeOpacity: regl.prop('rendering.wireframeOpacity'),
useBorder: (ctx, props) => props.border.width > 0,
lineColor: regl.prop('lineColor'),
borderColor: regl.prop('borderColor'),
dashColor: regl.prop('dashColor'),
squareCap: (ctx, props) => props.cap === 'square',
stretch: regl.prop('geometry.stretch'),
flip: regl.prop('geometry.flip'),
pixelRatio: regl.context('pixelRatio'),
lineWidth: (ctx, props) => ctx.pixelRatio * props.line.width,
borderWidth: (ctx, props) => ctx.pixelRatio * props.border.width,
dashLength: (ctx, props) => props.line.width * props.dash.length * 2.0,
},
blend: {
enable: true,
func: {
srcRGB: 'src alpha',
srcAlpha: 1,
dstRGB: 'one minus src alpha',
dstAlpha: 1
}
},
cull: {
enable: (ctx, props) => props.rendering.cull !== 'none',
face: (ctx, props) => props.rendering.cull === 'none' ? 'front' : props.rendering.cull
},
depth: {
enable: (ctx, props) => !!props.rendering.depth
},
primitive
});
}
return commandCache[cacheKey];
}
function updateBuffers () {
//lineData.vertexAttributes.xy.subdata(path);
//lineData.endpointAttributes.xy.subdata([path.slice(0, 3), path.slice(-3).reverse()]);
lineData.vertexAttributes.dist.subdata(dist);
lineData.endpointAttributes.dist.subdata([dist.slice(0, 3), dist.slice(-3).reverse()]);
}
function draw () {
computeCumulativeDistance(dist, path, project);
updateBuffers();
regl.poll();
regl.clear({color: [0.2, 0.2, 0.2, 1], depth: 1});
getDrawLines({})({
...lineData,
...state.lineConfig,
...state,
lineColor: [0.3, 0.2, 0.8, state.line.opacity],
borderColor: [0, 0, 0, state.border.opacity],
dashColor: [0, 0, 0, state.dash.opacity],
primitive: 'triangle strip',
numberOffset: 0,
});
if (state.rendering.labelPoints) {
getDrawLines({primitive: 'points'})({
...lineData,
...state.lineConfig,
...state,
lineColor: [0, 0, 0, state.line.opacity],
borderColor: [0, 0, 0, state.border.opacity],
dashColor: [0, 0, 0, state.dash.opacity],
primitive: 'points',
numberOffset: 15
});
}
}
computeCumulativeDistance(dist, path, project);
draw();
window.addEventListener('resize', draw);