SOURCE

console 命令行工具 X clear

                    
>
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>