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

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