Published
Edited
Jun 7, 2021
1 fork
Insert cell
md`# WebGL Lines(With LineCap)`
Insert cell
canvas = DOM.canvas(width, height);
Insert cell
{
draw()
}
Insert cell
Insert cell
Insert cell
lineJoin = LINE_JOINS.bevel
Insert cell
lineCap = LINE_CAPS.round
Insert cell
Insert cell
Insert cell
shapeData = ({
positions: [
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
0.5, 0.5
],
indices: [
0, 1, 2,
1, 3, 2
]
})
Insert cell
data = ({
// 两个是可以复用的
startPos: [
[200, 500],
[200, 500],
[400, 100],
],
endPos: [
[0, 0],
[400, 100],
[600, 500],
[600, 500],
],
lineWidth: [100, 100, 100]
})
Insert cell
startPosBuffer = createBuffer(gl, data.startPos.flat())
Insert cell
endPosBuffer = createBuffer(gl, data.endPos.flat())
Insert cell
lineWidthBuffer = createBuffer(gl, data.lineWidth)
Insert cell
shapePosBuffer = createBuffer(gl, shapeData.positions)
Insert cell
ebo = createIndexBuffer(gl, shapeData.indices)
Insert cell
attributeLocations = {
return {
position: gl.getAttribLocation(program, "in_position"),
startPos: gl.getAttribLocation(program, "in_startPos"),
endPos: gl.getAttribLocation(program, "in_endPos"),
prevPos: gl.getAttribLocation(program, "in_prevPos"),
nextPos: gl.getAttribLocation(program, "in_nextPos"),
lineWidth: gl.getAttribLocation(program, "in_lineWidth"),
}
}
Insert cell
uniformLocations = {
return {
resolution: gl.getUniformLocation(program, "u_resolution"),
lineJoin: gl.getUniformLocation(program, "u_lineJoin"),
lineCap: gl.getUniformLocation(program, "u_lineCap"),
}
}
Insert cell
program = createProgram(gl, vertexShaderSource, fragmentShaderSource)
Insert cell
gl = canvas.getContext('webgl2')
Insert cell
height = width * 0.75
Insert cell
width
Insert cell
vertexShaderSource = `#version 300 es

in vec2 in_position;
in vec2 in_startPos;
in vec2 in_endPos;
in vec2 in_prevPos;
in vec2 in_nextPos;
in float in_lineWidth;

out vec2 v_startPos;
out vec2 v_endPos;
out vec2 v_startMiterVec;
out vec2 v_endMiterVec;
out float v_lineWidth;


uniform vec2 u_resolution;

mat3 getTransformMatrix(vec2 startPos, vec2 endPos, float lineWidth) {
vec2 centerPos = (startPos + endPos) / 2.;
vec2 delta = endPos - startPos;
float len = length(delta);
float phi = atan(delta.y / delta.x);
mat3 scale = mat3(
len, 0, 0,
0, lineWidth, 0,
0, 0, 1
);

mat3 rotate = mat3(
cos(phi), sin(phi), 0,
-sin(phi), cos(phi), 0,
0, 0, 1
);
mat3 translate = mat3(
1, 0, 0,
0, 1, 0,
centerPos.x, centerPos.y, 1
);
return translate * rotate * scale;
}

vec2 getOffsetVec(vec2 pos, vec2 prev, vec2 next) {
if (pos == prev || pos == next) {
return vec2(0., 0.);
}
vec2 line1 = pos - prev;
vec2 normal1 = normalize(vec2(-line1.y, line1.x));
vec2 line2 = next - pos;
vec2 normal2 = normalize(vec2(-line2.y, line2.x));
vec2 normal = normalize(normal1 + normal2);
vec2 vec = normal * 1. / abs(dot(normal, normal1));
return -vec; // 逆时针向外的向量
}

void main() {
vec2 v1 = getOffsetVec(in_startPos, in_prevPos, in_endPos) * in_lineWidth / 2.;
vec2 v2 = getOffsetVec(in_endPos, in_startPos, in_nextPos) * in_lineWidth / 2.;
vec2 dir = normalize(in_endPos - in_startPos);
vec2 startOffset = (v1 == vec2(0., 0.) ? -in_lineWidth / 2. : dot(v1, dir)) * dir;
vec2 endOffset = (v2 == vec2(0., 0.) ? in_lineWidth / 2. : dot(v2, dir)) * dir;
mat3 transformMatrix = getTransformMatrix(in_startPos + startOffset, in_endPos + endOffset, in_lineWidth);
vec2 pos = (transformMatrix * vec3(in_position, 1.)).xy;
// vec2 pos = in_position * in_lineWidth + in_startPos;
// vec4 pos = u_transform * vec4(in_position, 0., 1.);

// convert the position from pixels to 0.0 to 1.0
vec2 zeroToOne = pos.xy / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;

gl_Position = vec4(clipSpace, 0, 1);

v_startPos = in_startPos;
v_endPos = in_endPos;
v_startMiterVec = v1;
v_endMiterVec = v2;
v_lineWidth = in_lineWidth;
}
`
Insert cell
fragmentShaderSource = `#version 300 es

precision highp float;

in vec2 v_startPos;
in vec2 v_endPos;
in vec2 v_startMiterVec;
in vec2 v_endMiterVec;
in float v_lineWidth;

out vec4 fragColor;

uniform float u_lineJoin; // 0: miter; 1: bevel; 2: round;
uniform float u_lineCap; // 0: none; 1: butt; 2: round;

void main() {
vec2 p = gl_FragCoord.xy;

// TODO: 需要重构,逻辑比较混乱
if (v_startMiterVec == vec2(0., 0.)) {
// 需要判断在线端点内还是外
bool outStartMainLine = dot((p - v_startPos), (v_endPos - v_startPos)) < 0.;
if (u_lineCap == 0.) {
if (outStartMainLine) {
discard;
}
} else if (u_lineCap == 2.) {
if (outStartMainLine && distance(p, v_startPos) > v_lineWidth / 2.) {
discard;
}
}
// butt的情况不用额外的操作
}

if (v_endMiterVec == vec2(0., 0.)) {
// 需要判断在线端点内还是外
bool outEndMainLine = dot((p - v_endPos), (v_startPos - v_endPos)) < 0.;
if (u_lineCap == 0.) {
if (outEndMainLine) {
discard;
}
} else if (u_lineCap == 2.) {
if (outEndMainLine && distance(p, v_endPos) > v_lineWidth / 2.) {
discard;
}
}
// butt的情况不用额外的操作
}

vec2 miterNormal1 = vec2(-v_startMiterVec.y, v_startMiterVec.x);
vec2 miterNormal2 = vec2(-v_endMiterVec.y, v_endMiterVec.x);
// TODO: 可能不够robust
bool outside = dot((p - v_startPos), miterNormal1) < 0. || dot((p - v_endPos), miterNormal2) > 0.;
if (outside) {
discard;
}

if (u_lineJoin != 0.) { // mitter: outside就可以判断

// 需要判断在线端点内还是外
bool outStartMainLine = dot((p - v_startPos), (v_endPos - v_startPos)) < 0.;
bool outEndMainLine = dot((p - v_endPos), (v_startPos - v_endPos)) < 0.;
if (u_lineJoin == 1.) {
// round: 考虑与端点的距离即可
if (outStartMainLine && distance(p, v_startPos) > v_lineWidth / 2. || outEndMainLine && distance(p, v_endPos) > v_lineWidth / 2.) {
discard;
}
} else if (u_lineJoin == 2.) {
// bevel: 需要计算bevel处的曲线
vec2 lineVec = normalize(v_endPos - v_startPos);
vec2 lineNormal = vec2(-lineVec.y, lineVec.x);
vec2 startBevelCenter = v_startPos + abs(dot(lineNormal, normalize(v_startMiterVec))) * v_lineWidth / 2. * normalize(v_startMiterVec);
vec2 endBevelCenter = v_endPos + abs(dot(lineNormal, normalize(v_endMiterVec))) * v_lineWidth / 2. * normalize(v_endMiterVec);

if (outStartMainLine && dot((p - startBevelCenter), (v_startPos - startBevelCenter)) < 0. || outEndMainLine && dot((p - endBevelCenter), (v_endPos - endBevelCenter)) < 0.) {
discard;
}
}
}

fragColor = vec4(0., 0., 0., 1.);
}
`
Insert cell
function createIndexBuffer(gl, indices) {
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW)
return buffer
}
Insert cell
function createBuffer(gl, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
return buffer;
}
Insert cell
function createProgram(gl, vertShaderStr, fragShaderStr) {
const vertShader = compileShader(gl, vertShaderStr, gl.VERTEX_SHADER)
const fragShader = compileShader(gl, fragShaderStr, gl.FRAGMENT_SHADER)

const program = gl.createProgram()

gl.attachShader(program, vertShader)
gl.attachShader(program, fragShader)

gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(`Could not link shaders: ${gl.getProgramInfoLog(program)}`)
}

return program
}
Insert cell
function compileShader(gl, shaderStr, shaderType) {
const shader = gl.createShader(shaderType)
gl.shaderSource(shader, shaderStr)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error('Shader compile failed: ' + gl.getShaderInfoLog(shader))
}

return shader
}
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