Published unlisted
Edited
Jan 15, 2021
Importers
1 star
Insert cell
Insert cell
xy = t.map(t => [t * 1, 1.5 * Math.sin(Math.PI * t * t + 1.2), 0])
/*xy = [
[-5, 0.5, 0],
[-4, 1, 0],
[-3.01, 1, 0],
[-3, 1, 0],
[-2, 1, 0],
[1.21, -1, 0],
[0.25, 1.4, 0],
[0.29, 0, 0],
[2.25, 2, 0]
].flat()*/
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const { canvas, regl, camera } = plot1;
const drawLines = createDrawLinesCommand(regl);
const configureDrawing = createDrawingConfigurationCommand(regl);

regl.poll();
regl.clear({ color: [1, 1, 1, 1] });
camera(() => {
configureDrawing(() => {
// Draw the nicely mitred/beveled lines
drawLines({
position: xy,
width: { constant: lineWidth },
//color: { constant: hexToFloatRgba(lineColor, alpha) },
mitreLimit
});
// Draw a black line in the center, for reference
drawLines({
position: xy,
width: { constant: 1 },
color: { constant: [0, 0, 0, 1] },
mitreLimit
});
});
});

return canvas;
}
Insert cell
function createDrawLinesCommand(regl, opts) {
opts = opts || {};
opts.uniforms = opts.uniforms || {};
opts.positionFunction =
opts.positionFunction ||
`
uniform mat4 uProjectionViewModel;
vec4 computePosition (vec3 p) {
return uProjectionViewModel * vec4(p, 1);
}
`;
const configure = createConfigureLinesCommand(regl, opts);
const drawLines = createDrawLineSegmentsCommand(regl, opts);
const drawBevels = createDrawBevelsCommand(regl, opts);
const drawEnds = createDrawEndsCommand(regl, opts);

return function(lines) {
configure(lines, () => {
drawBevels(lines);
drawLines(lines);
drawEnds(lines, true);
drawEnds(lines, false);
});
};
}
Insert cell
createConfigureLinesCommand = function(regl, opts) {
const DEFAULT_LINE_WIDTH = 2;
const DEFAULT_MITRE_LIMIT = 3;
return regl({
frag: `
precision highp float;
varying vec4 vColor;
void main () {
gl_FragColor = vColor;
}`,
uniforms: Object.assign(
{
uAspect: ctx => ctx.viewportWidth / ctx.viewportHeight,
uScaleFactor: (ctx, props) => ctx.pixelRatio / ctx.viewportHeight,
uMitreLimit: (ctx, props) =>
(props.bevel === undefined
? true
: !!props.bevel)
? props.mitreLimit === undefined
? DEFAULT_MITRE_LIMIT
: props.mitreLimit
: 1e8
},
opts.uniforms
),
primitive: 'triangle strip'
});
}
Insert cell
createDrawLineSegmentsCommand = function(regl, opts) {
const DEFAULT_LINE_COLOR = [0.3, 0.4, 0.9, 0.9];
const DEFAULT_LINE_WIDTH = 2;
const unsplitBuffer = regl.buffer([[0, -1], [1, -1], [0, 1], [1, 1]]);

return regl({
vert: `
precision highp float;
${opts.positionFunction}
uniform float uAspect, uScaleFactor, uMitreLimit;

attribute vec3 aP0, aP1, aP2, aP3;
attribute float aWidth1, aWidth2;
attribute vec4 aColor1, aColor2;
attribute vec2 aLinePosition;

varying vec4 vColor;
varying vec2 vStrokeEdges;

vec2 lineNormal (vec4 a, vec4 b, float aspect) {
return normalize((a.yx / a.w - b.yx / b.w) * vec2(1, aspect));
}

void main () {
bool isStart = aLinePosition.x < 0.5;

float lineWidth = isStart ? aWidth1 : aWidth2;
vColor = isStart ? aColor1 : aColor2;

// Compute the position of two adjacent points
vec4 p1 = computePosition(aP1);
vec4 p2 = computePosition(aP2);

// Compute the position of an adjacent point--the preceeding point
// if the start of the segment, the next point otherwise.
vec4 pE = computePosition(isStart ? aP0 : aP3);

vec2 n12 = lineNormal(p1, p2, uAspect);
vec2 t12 = vec2(-n12.y, n12);

// Compute the normal of the adjacent (previous or next) segment
vec2 nE = lineNormal(isStart ? pE : p2, isStart ? p1 : pE, uAspect);

// Compute the bisector normal at the vertex
vec2 n = normalize(nE + n12);

// Compute the orientation of the normal with respect to the which
// corner of the line segment quad we're on
float orientation = sign(aLinePosition.y) * (isStart ? 1.0 : -1.0);

// Scale the normal. Unfortunately we need to jump through a bunch of hoops here in order to
// get the difference between > and >= just right so that straight lines don't flip the bevel
// incorrectly.
n *= (dot(t12, n) * (isStart ? 1.0 : -1.0) > 0.0 ? 1.0 : -1.0) * (isStart ? orientation : -orientation);

// Compute the extension of the full mitre
float mitreSize = 1.0 / dot(n, n12);

// Scale the mean normal, then step back along
n *= mitreSize;

float maxMitre = -dot(n, vec2(-nE.y, nE.x)) * orientation;
mitreSize = max(0.0, min(maxMitre, mitreSize - uMitreLimit));

// If we've overextended beyond the mitre limit, step back along
// the segment tangent by the necessary amount
n -= t12 * orientation * mitreSize;

gl_Position = mix(p1, p2, aLinePosition.x);
gl_Position.xy += (n / vec2(-uAspect, 1) * aLinePosition.y * lineWidth) * gl_Position.w * uScaleFactor;
}`,
attributes: {
aLinePosition: unsplitBuffer,
aP0: attr('position', null, 3, 0),
aP1: attr('position', null, 3, 1),
aP2: attr('position', null, 3, 2),
aP3: attr('position', null, 3, 3),
aWidth1: attr('width', DEFAULT_LINE_WIDTH, 1, 1),
aWidth2: attr('width', DEFAULT_LINE_WIDTH, 1, 2),
aColor1: attr('color', DEFAULT_LINE_COLOR, 4, 1),
aColor2: attr('color', DEFAULT_LINE_COLOR, 4, 2)
},
instances: (ctx, props) => {
if (props.count !== undefined) return props.count - 3;
if (
props.position[0] !== undefined &&
props.position[0].length !== undefined
)
return props.position.length - 3;
if (props.position.length !== undefined)
return props.position.length / 3 - 3;
return null;
},
count: 4
});
}
Insert cell
createDrawBevelsCommand = function(regl, opts) {
const DEFAULT_LINE_COLOR = [0.9, 0.4, 0.3, 1];
const DEFAULT_LINE_WIDTH = 2;
const unsplitBuffer = [[0, -1], [1, 1], [-1, 1]];

return regl({
vert: `
precision highp float;
${opts.positionFunction}

uniform float uAspect, uScaleFactor, uMitreLimit;

attribute vec3 aP0, aP1, aP2;
attribute float aWidth1, aWidth2;
attribute vec4 aColor1, aColor2;
attribute vec2 aLinePosition;

varying vec4 vColor;
varying vec2 vStrokeEdges;

vec2 lineNormal (vec4 a, vec4 b, float aspect) {
return normalize((a.yx / a.w - b.yx / b.w) * vec2(1, aspect));
}

void main () {
vColor = aColor1;

// Compute the position of two adjacent points
vec4 p0 = computePosition(aP0);
vec4 p1 = computePosition(aP1);
vec4 p2 = computePosition(aP2);

vec2 n01 = lineNormal(p0, p1, uAspect);
vec2 n12 = lineNormal(p1, p2, uAspect);
vec2 n = normalize(n01 + n12);

vec2 nAdj = aLinePosition.x > 0.0 ? n12 : n01;
vec2 tAdj = vec2(-nAdj.y, nAdj.x);
float mitreSize = 1.0 / dot(n, n01);

bool isOuter = dot(n01, vec2(-n12.y, n12.x)) > 0.0;
n *= mitreSize * (isOuter ? 1.0 : -1.0);
float maxMitre = dot(n, tAdj) * aLinePosition.x;

n -= tAdj * aLinePosition.x * max(0.0, min(maxMitre, mitreSize - uMitreLimit));

gl_Position = p1;
gl_Position.xy += (n / vec2(-uAspect, 1) * aWidth1) * aLinePosition.y * gl_Position.w * uScaleFactor;
}`,
attributes: {
aLinePosition: unsplitBuffer,
aP0: attr('position', null, 3, 0),
aP1: attr('position', null, 3, 1),
aP2: attr('position', null, 3, 2),
aWidth1: attr('width', DEFAULT_LINE_WIDTH, 1, 1),
aColor1: attr('color', DEFAULT_LINE_COLOR, 4, 1)
},
instances: (ctx, props) => {
if (props.count !== undefined) return props.count - 2;
if (
props.position[0] !== undefined &&
props.position[0].length !== undefined
)
return props.position.length - 2;
if (props.position.length !== undefined)
return props.position.length / 3 - 2;
return null;
},
count: 3
});
}
Insert cell
createDrawEndsCommand = function(regl, opts) {
const DEFAULT_LINE_COLOR = [0.4, 0.9, 0.3, 0.9];
const DEFAULT_LINE_WIDTH = 2;

const unsplitBuffer = regl.buffer([[0, -1], [1, -1], [0, 1], [1, 1]]);
let isStart = false;

var tmp = {};
const attr = function attr(
propName,
defaultValue,
defaultStride,
pointOffset
) {
return function(ctx, props) {
var count;
var value = props[propName];

if (!isStart) {
if (props.count !== undefined) {
count = props.count;
} else if (
props.position[0] !== undefined &&
props.position[0].length !== undefined
) {
count = props.position.length;
} else {
count = props.position.length / 3;
}
}
if (value === undefined) return { constant: defaultValue };
if (value.constant !== undefined) return props[propName];
var p = (tmp.buffer = props[propName]);
if (!p.buffer) {
tmp.buffer = p;
p = props;
}
var stride =
p.stride === undefined
? defaultStride * Float32Array.BYTES_PER_ELEMENT
: p.stride;
var offset = p.offset === undefined ? 0 : p.offset;
var ptOffset = pointOffset || 0;

if (!isStart) ptOffset += count - 3;

tmp.stride = stride;
tmp.offset = offset + ptOffset * stride;
tmp.divisor = 1;
return tmp;
};
};

const cmd = regl({
vert: `
precision highp float;
${opts.positionFunction}
uniform float uAspect, uScaleFactor, uMitreLimit;
uniform bool uFlip;

attribute vec3 aP0, aP1, aP2;
attribute float aWidth1, aWidth2;
attribute vec4 aColor1, aColor2;
attribute vec2 aLinePosition;

varying vec4 vColor;
varying vec2 vStrokeEdges;

vec2 lineNormal (vec4 a, vec4 b, float aspect) {
return normalize((a.yx / a.w - b.yx / b.w) * vec2(1, aspect));
}

void main () {
bool start = aLinePosition.x < 0.5;
float lineWidth = mix(aWidth1, aWidth2, aLinePosition.x);
vColor = mix(aColor1, aColor2, aLinePosition.x);

// Compute the position of two adjacent points
vec4 p0 = computePosition(uFlip ? aP2 : aP0);
vec4 p1 = computePosition(aP1);
vec4 p2 = computePosition(uFlip ? aP0 : aP2);

vec2 n01 = lineNormal(p0, p1, uAspect);
vec2 n12 = lineNormal(p1, p2, uAspect);
vec2 t01 = vec2(-n01.y, n01.x);

vec2 n = normalize(n01 + n12);

float mitreSize = 1.0 / dot(n12, n);
n *= mitreSize;
float f = dot(n, t01);
float maxMitre = -f * aLinePosition.y;
n -= t01 * sign(f) * max(0.0, min(maxMitre, mitreSize - uMitreLimit));
n = mix(n01, n, aLinePosition.x);

gl_Position = mix(p0, p1, aLinePosition.x);
gl_Position.xy += (n / vec2(-uAspect, 1) * aLinePosition.y * lineWidth) * gl_Position.w * uScaleFactor;
}`,
attributes: {
aLinePosition: unsplitBuffer,
aP0: attr('position', null, 3, 0),
aP1: attr('position', null, 3, 1),
aP2: attr('position', null, 3, 2),
aWidth1: attr('width', DEFAULT_LINE_WIDTH, 1, 0),
aWidth2: attr('width', DEFAULT_LINE_WIDTH, 1, 1),
aColor1: attr('color', DEFAULT_LINE_COLOR, 4, 0),
aColor2: attr('color', DEFAULT_LINE_COLOR, 4, 1)
},
uniforms: {
uFlip: () => !isStart
},
instances: 1,
count: 4
});

return function(lines, isStart_) {
isStart = isStart_;
cmd(lines);
};
}
Insert cell
function createDrawingConfigurationCommand(regl) {
return regl({
blend: {
enable: true,
func: {
srcRGB: 'src alpha',
dstRGB: 'one minus src alpha',
srcAlpha: 'src alpha',
dstAlpha: 'one minus src alpha'
},
equation: { rgb: 'add', alpha: 'add' }
},
depth: { enable: false }
});
}
Insert cell
attr = {
var tmp = {};
return function attr(
propName,
defaultValue,
defaultStride,
pointOffset,
endpoints
) {
return function(ctx, props) {
var value = props[propName];
if (value === undefined) return { constant: defaultValue };
if (value.constant !== undefined) return props[propName];
var p = (tmp.buffer = props[propName]);
if (!p.buffer) {
tmp.buffer = p;
p = props;
}
var stride =
p.stride === undefined
? defaultStride * Float32Array.BYTES_PER_ELEMENT
: p.stride;
var offset = p.offset === undefined ? 0 : p.offset;
tmp.stride = stride;
tmp.offset = offset + (pointOffset || 0) * stride;
tmp.divisor = 1;
return tmp;
};
};
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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