console
/*
Stroke with miter/bevel joints generated with vertex shader and fragment shader.
Only very limited CPU processing is involved.
References:
https://developers.arcgis.com/javascript/latest/sample-code/custom-gl-animated-lines/index.html
*/
const W = 640;
const H = 360;
const vsSource = `
uniform float uWidth;
uniform float uTolerance;
uniform vec3 uColor;
attribute vec2 aPos;
attribute vec2 aPrev;
attribute vec2 aNext;
attribute float aOrder;
varying highp vec3 vColor;
vec2 ndcFromScreen(vec2 screen) {
return vec2(screen.x / ${W}. * 2. - 1., -screen.y / ${H}. * 2. + 1.);
}
void main() {
vec2 vPrev = (aPos - aPrev); vPrev = normalize(vPrev);
vec2 vNext = (aPos - aNext); vNext = normalize(vNext);
vec2 nPrev = vec2(-vPrev.y, vPrev.x);
vec2 nNext = vec2(vNext.y, -vNext.x);
vec2 n = nPrev + nNext; n = normalize(n);
float cosTheta = dot(n, nPrev) / length(n) / length(nPrev);
float extrudedLen = abs(uWidth / 2. / cosTheta);
bool turnDir = dot(vPrev, vNext) > 0.;
bool isAbove = aOrder == 0. || aOrder == 2.;
bool isLeft = aOrder <= 1.;
n = n * extrudedLen;
if (!isAbove) n = -n;
vec2 pos = aPos + n;
if ((isAbove && turnDir) || (!isAbove && !turnDir)) {
float limit = uWidth / 2. * uTolerance;
if (extrudedLen > limit) {
vec2 nRef = (isLeft ? nPrev : nNext) * uWidth / 2.;
if (!isAbove) nRef = -nRef;
vec2 q = nRef - n;
float x = extrudedLen * (extrudedLen - limit) / length(q);
pos += normalize(q) * x;
}
}
gl_Position = vec4(ndcFromScreen(pos), 0.0, 1.0);
vColor = uColor;
}
`;
const fsSource = `
varying highp vec3 vColor;
void main() {
gl_FragColor = vec4(vColor.rgb, 1.);
}
`;
const canvas = document.createElement("canvas");
canvas.width = W;
canvas.height = H;
document.body.appendChild(canvas);
const gl = canvas.getContext("webgl2");
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vsSource);
gl.compileShader(vertexShader);
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(vertexShader));
}
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fsSource);
gl.compileShader(fragmentShader);
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(fragmentShader));
}
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert(gl.getProgramInfoLog(shaderProgram));
}
const polyline = [100, 100, 200, 100, 300, 200, 300, 50, 500, 50];
const polylineSize = polyline.length / 2;
// Extend the first and last points for prev/next information
const polylineExt = polyline.slice();
polylineExt.unshift(2 * polyline[1] - polyline[3]);
polylineExt.unshift(2 * polyline[0] - polyline[2]);
polylineExt.push(
2 * polyline[polyline.length - 2] - polyline[polyline.length - 4]
);
polylineExt.push(
2 * polyline[polyline.length - 1] - polyline[polyline.length - 3]
);
const polylineExtSize = polylineExt.length / 2;
// Every point on the polyline is extended to 4 points (some are redundant), two on each side.
const stroke = [];
for (let i = 0; i < polylineExtSize; i++) {
for (let j = 0; j < 4; j++) {
stroke.push(polylineExt[i * 2]);
stroke.push(polylineExt[i * 2 + 1]);
}
}
// Since gl_VertexID is not supported, we have to assign the
// vertex orders manually.
const order = []; // lt(left-top), lb, rt, rb
for (let i = 0; i < polylineSize; i++) {
for (let j = 0; j < 4; j++) {
order.push(j);
}
}
const dists = []; // Distances along the polyline
for (let i = 0; i < polylineSize; i++) {
for (let j = 0; j < 4; j++) {
const dx = polyline[(i - 1) * 2] - polyline[i * 2];
const dy = polyline[(i - 1) * 2 + 1] - polyline[i * 2 + 1];
dists.push(i === 0 ? 0 : dists[(i - 1) * 4] + Math.sqrt(dx * dx + dy * dy));
}
}
// To save storage, current/prev/next positions share the buffer
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(stroke), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(shaderProgram, "aPos");
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 8 * 4);
const aPrev = gl.getAttribLocation(shaderProgram, "aPrev");
gl.enableVertexAttribArray(aPrev);
gl.vertexAttribPointer(aPrev, 2, gl.FLOAT, false, 0, 0);
const aNext = gl.getAttribLocation(shaderProgram, "aNext");
gl.enableVertexAttribArray(aNext);
gl.vertexAttribPointer(aNext, 2, gl.FLOAT, false, 0, 8 * 4 * 2);
const aOrder = gl.getAttribLocation(shaderProgram, "aOrder");
gl.enableVertexAttribArray(aOrder);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(order), gl.STATIC_DRAW);
gl.vertexAttribPointer(aOrder, 1, gl.FLOAT, false, 0, 0);
const aDist = gl.getAttribLocation(shaderProgram, "aDist");
gl.enableVertexAttribArray(aDist);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(dists), gl.STATIC_DRAW);
gl.vertexAttribPointer(aDist, 1, gl.FLOAT, false, 0, 0);
const uWidth = gl.getUniformLocation(shaderProgram, "uWidth");
gl.uniform1f(uWidth, 30.0);
const uTolerance = gl.getUniformLocation(shaderProgram, "uTolerance");
gl.uniform1f(uTolerance, 1.5);
const uColor = gl.getUniformLocation(shaderProgram, "uColor");
gl.uniform3f(uColor, 1.0, 0.0, 0.0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, polylineSize * 4);
gl.uniform3f(uColor, 1.0, 1.0, 0.0);
gl.drawArrays(gl.LINE_STRIP, 0, polylineSize * 4);
<body></body>