console
/*
This demo is based on my previous one "GPU Stroke Compiler".
Here I add texture mapping to the stroke, and correct UV coordinates to avoid
abrupt visual seams, using bilinear interpolation of quadrilaterals.
The primary limitation is that parameters are calculated per-quad. Unless
the points are repeated once more, bevel joints cannot show correctly.
References:
http://www.reedbeta.com/blog/quadrilateral-interpolation-part-2
*/
const W = 640;
const H = 360;
const vsSource = `
uniform float uWidth;
uniform float uTolerance;
attribute vec2 aPos;
attribute vec2 aPrev2;
attribute vec2 aPrev;
attribute vec2 aNext;
attribute vec2 aNext2;
attribute float aOrder; // 0, 1, 2, 3
varying highp vec2 vUv;
varying highp vec2 vQ;
varying highp vec2 vB1;
varying highp vec2 vB2;
varying highp vec2 vB3;
varying highp float vTexRepeatTimes;
vec2 calcQuadVertice(vec2 pos, vec2 prev, vec2 next, float order)
{
vec2 vPrev = (pos - prev); vPrev = normalize(vPrev);
vec2 vNext = (pos - next); 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 = order == 0. || order == 2.;
bool isLeft = order <= 1.;
n = n * extrudedLen;
if (!isAbove) n = -n;
pos = pos + 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;
}
}
return pos;
}
void main() {
vec2 pos = calcQuadVertice(aPos, aPrev, aNext, aOrder);
vec2 ndc = vec2(pos.x / ${W}. * 2. - 1., -pos.y / ${H}. * 2. + 1.);
gl_Position = vec4(ndc, 0.0, 1.0);
// As a reference only
vUv.x = aOrder < 2. ? 0. : 1.;
vUv.y = (aOrder == 0. || aOrder == 2.) ? 0. : 1.;
vTexRepeatTimes = floor(distance(aPos, aOrder < 2. ? aPrev : aNext) / uWidth);
vec2 quad[4];
if (aOrder > 1.) {
quad[0] = calcQuadVertice(aPos, aPrev, aNext, 2.);
quad[1] = calcQuadVertice(aNext, aPos, aNext2, 0.);
quad[2] = calcQuadVertice(aPos, aPrev, aNext, 3.);
quad[3] = calcQuadVertice(aNext, aPos, aNext2, 1.);
} else {
quad[0] = calcQuadVertice(aPrev, aPrev2, aPos, 2.);
quad[1] = calcQuadVertice(aPos, aPrev, aNext, 0.);
quad[2] = calcQuadVertice(aPrev, aPrev2, aPos, 3.);
quad[3] = calcQuadVertice(aPos, aPrev, aNext, 1.);
}
vQ = pos - quad[0];
vB1 = quad[1] - quad[0];
vB2 = quad[2] - quad[0];
vB3 = quad[0] - quad[1] - quad[2] + quad[3];
}
`;
const fsSource = `
precision highp float;
varying vec2 vUv;
varying vec2 vQ;
varying vec2 vB1;
varying vec2 vB2;
varying vec2 vB3;
varying float vTexRepeatTimes;
// A procedual texture representing a 2x2 chessboard
vec4 sample_virtual_texture(vec2 uv)
{
float x = clamp(floor(fract(uv.x) * 2.), 0., 1.);
float y = clamp(floor(fract(uv.y) * 2.), 0., 1.);
float t = (x + y) - 2. * (x * y); // arithmetic version of "x xor y"
return vec4(t, 0, 0, 1.);
}
float Wedge2D(vec2 v, vec2 w)
{
return v.x*w.y - v.y*w.x;
}
void main() {
// Set up quadratic formula
float A = Wedge2D(vB2, vB3);
float B = Wedge2D(vB3, vQ) - Wedge2D(vB1, vB2);
float C = Wedge2D(vB1, vQ);
vec2 uv;
if (abs(A) < 0.001)
{
// Linear form
uv.y = -C/B;
}
else
{
// Quadratic form. Take positive root for CCW winding with V-up
float discrim = B*B - 4.*A*C;
uv.y = 0.5 * (-B + sqrt(discrim)) / A;
}
// Solve for u, using largest-magnitude component
vec2 denom = vB1 + uv.y * vB3;
if (abs(denom.x) > abs(denom.y))
uv.x = (vQ.x - vB2.x * uv.y) / denom.x;
else
uv.x = (vQ.y - vB2.y * uv.y) / denom.y;
// Uncomment this to see the effect without correction
//uv = vUv;
uv.x *= vTexRepeatTimes;
gl_FragColor = sample_virtual_texture(uv);
}
`;
const canvas = document.createElement("canvas");
canvas.width = W;
canvas.height = H;
document.body.appendChild(canvas);
const gl = canvas.getContext("webgl");
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.unshift(0); // Placeholder
polylineExt.unshift(0);
polylineExt.push(
2 * polyline[polyline.length - 2] - polyline[polyline.length - 4]
);
polylineExt.push(
2 * polyline[polyline.length - 1] - polyline[polyline.length - 3]
);
polylineExt.push(0); // Placeholder
polylineExt.push(0);
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]);
}
}
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);
}
}
// To save storage, all position attributes share this buffer
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(stroke), gl.STATIC_DRAW);
const aPrev2 = gl.getAttribLocation(shaderProgram, "aPrev2");
gl.enableVertexAttribArray(aPrev2);
gl.vertexAttribPointer(aPrev2, 2, gl.FLOAT, false, 0, 8 * 4 * 0);
const aPrev = gl.getAttribLocation(shaderProgram, "aPrev");
gl.enableVertexAttribArray(aPrev);
gl.vertexAttribPointer(aPrev, 2, gl.FLOAT, false, 0, 8 * 4 * 1);
const aPos = gl.getAttribLocation(shaderProgram, "aPos");
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 8 * 4 * 2);
const aNext = gl.getAttribLocation(shaderProgram, "aNext");
gl.enableVertexAttribArray(aNext);
gl.vertexAttribPointer(aNext, 2, gl.FLOAT, false, 0, 8 * 4 * 3);
const aNext2 = gl.getAttribLocation(shaderProgram, "aNext2");
gl.enableVertexAttribArray(aNext2);
gl.vertexAttribPointer(aNext2, 2, gl.FLOAT, false, 0, 8 * 4 * 4);
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 uWidth = gl.getUniformLocation(shaderProgram, "uWidth");
gl.uniform1f(uWidth, 30.0);
const uTolerance = gl.getUniformLocation(shaderProgram, "uTolerance");
gl.uniform1f(uTolerance, 100000); //1.5
gl.drawArrays(gl.TRIANGLE_STRIP, 0, polylineSize * 4);
<body></body>