@vector-display/webgl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/WebGLVectorRenderer.d.ts +36 -0
- package/dist/WebGLVectorRenderer.d.ts.map +1 -0
- package/dist/WebGLVectorRenderer.js +137 -0
- package/dist/WebGLVectorRenderer.js.map +1 -0
- package/dist/beamShaders.d.ts +3 -0
- package/dist/beamShaders.d.ts.map +1 -0
- package/dist/beamShaders.js +105 -0
- package/dist/beamShaders.js.map +1 -0
- package/dist/createShaderProgram.d.ts +6 -0
- package/dist/createShaderProgram.d.ts.map +1 -0
- package/dist/createShaderProgram.js +30 -0
- package/dist/createShaderProgram.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
- package/src/WebGLVectorRenderer.ts +178 -0
- package/src/beamShaders.ts +105 -0
- package/src/createShaderProgram.ts +39 -0
- package/src/index.ts +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rick Segrest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @vector-display/webgl
|
|
2
|
+
|
|
3
|
+
WebGL2 renderer for [**vector-display**](https://www.npmjs.com/package/@vector-display/core). It draws an entire display list in a single draw call, with anti-aliased beams, a soft glow, and optional bright endpoints like a real vector monitor.
|
|
4
|
+
|
|
5
|
+
> **Status:** early (0.x). The API may change between minor versions.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @vector-display/core @vector-display/webgl
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
ES modules only, with TypeScript declarations. Requires a browser with WebGL2.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { DisplayList, Shape } from "@vector-display/core";
|
|
19
|
+
import { WebGLVectorRenderer } from "@vector-display/webgl";
|
|
20
|
+
|
|
21
|
+
const renderer = WebGLVectorRenderer.fromCanvas(canvas, { width: 1024, height: 768 });
|
|
22
|
+
renderer.setLineStyle({ beamWidth: 1.5 * devicePixelRatio, glowRadius: 2 * devicePixelRatio });
|
|
23
|
+
|
|
24
|
+
const displayList = new DisplayList();
|
|
25
|
+
const square = Shape.fromPolyline({
|
|
26
|
+
points: [{ x: -10, y: -10 }, { x: 10, y: -10 }, { x: 10, y: 10 }, { x: -10, y: 10 }],
|
|
27
|
+
isClosed: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function drawFrame() {
|
|
31
|
+
displayList.clear();
|
|
32
|
+
displayList.setColor({ red: 0, green: 1, blue: 0 });
|
|
33
|
+
displayList.addShape(square, { x: 512, y: 384, rotation: 0, scale: 4, intensity: 1 });
|
|
34
|
+
renderer.clear();
|
|
35
|
+
renderer.drawDisplayList(displayList);
|
|
36
|
+
requestAnimationFrame(drawFrame);
|
|
37
|
+
}
|
|
38
|
+
requestAnimationFrame(drawFrame);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The canvas's pixel size is up to you (for example CSS size × `devicePixelRatio`); the renderer maps the world size you pass onto it.
|
|
42
|
+
|
|
43
|
+
## Line style
|
|
44
|
+
|
|
45
|
+
`renderer.setLineStyle({...})` accepts any of these (sizes in framebuffer pixels):
|
|
46
|
+
|
|
47
|
+
| Field | Default | Effect |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `beamWidth` | 1.5 | Core line width |
|
|
50
|
+
| `glowRadius` / `glowStrength` | 4 / 0.25 | Soft halo around each line |
|
|
51
|
+
| `endpointBrightness` | 0.6 | Brightness added at segment endpoints (0 removes it) |
|
|
52
|
+
| `jointOverlap` | 1 | Brightness added where connected segments meet (0 gives seamless joints) |
|
|
53
|
+
|
|
54
|
+
## Using it with other libraries
|
|
55
|
+
|
|
56
|
+
- `new WebGLVectorRenderer(gl, worldSize)` accepts an existing `WebGL2RenderingContext`.
|
|
57
|
+
- `drawDisplayList()` draws additively into whatever framebuffer is bound and doesn't clear it, so you can render into your own texture.
|
|
58
|
+
- `createShaderProgram(gl, { vertexSource, fragmentSource })` is exported for effect code.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT © Rick Segrest
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type DisplayList } from "@vector-display/core";
|
|
2
|
+
export interface LineStyle {
|
|
3
|
+
readonly beamWidth: number;
|
|
4
|
+
readonly glowRadius: number;
|
|
5
|
+
readonly glowStrength: number;
|
|
6
|
+
readonly endpointBrightness: number;
|
|
7
|
+
readonly jointOverlap: number;
|
|
8
|
+
}
|
|
9
|
+
export interface WorldSize {
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_LINE_STYLE: LineStyle;
|
|
14
|
+
export declare class WebGLVectorRenderer {
|
|
15
|
+
readonly gl: WebGL2RenderingContext;
|
|
16
|
+
private readonly program;
|
|
17
|
+
private readonly uniforms;
|
|
18
|
+
private readonly vertexArray;
|
|
19
|
+
private readonly segmentBuffer;
|
|
20
|
+
private segmentBufferCapacityBytes;
|
|
21
|
+
private worldSize;
|
|
22
|
+
private lineStyle;
|
|
23
|
+
constructor(gl: WebGL2RenderingContext, worldSize: WorldSize);
|
|
24
|
+
static fromCanvas(canvas: HTMLCanvasElement, worldSize: WorldSize): WebGLVectorRenderer;
|
|
25
|
+
setWorldSize(worldSize: WorldSize): void;
|
|
26
|
+
setLineStyle(lineStyle: Partial<LineStyle>): void;
|
|
27
|
+
clear(): void;
|
|
28
|
+
drawDisplayList(displayList: DisplayList): void;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
private findUniformLocations;
|
|
31
|
+
private createSegmentVertexArray;
|
|
32
|
+
private uploadSegmentData;
|
|
33
|
+
private applyUniforms;
|
|
34
|
+
}
|
|
35
|
+
export default WebGLVectorRenderer;
|
|
36
|
+
//# sourceMappingURL=WebGLVectorRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebGLVectorRenderer.d.ts","sourceRoot":"","sources":["../src/WebGLVectorRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKjF,MAAM,WAAW,SAAS;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IAEpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,SAAS;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,kBAAkB,EAAE,SAMhC,CAAC;AAyBF,qBAAa,mBAAmB;IAC5B,SAAgB,EAAE,EAAE,sBAAsB,CAAC;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IACrD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAc;IAC5C,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,SAAS,CAAiC;IAElD,YAAY,EAAE,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS,EAU3D;IAED,OAAc,UAAU,CAAC,MAAM,EAAE,iBAAiB,EAAE,SAAS,EAAE,SAAS,GAAG,mBAAmB,CAS7F;IAEM,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAE9C;IAEM,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAEvD;IAEM,KAAK,IAAI,IAAI,CAKnB;IAGM,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAcrD;IAEM,OAAO,IAAI,IAAI,CAIrB;IAED,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,wBAAwB;IAehC,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,aAAa;CAaxB;eAEc,mBAAmB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { FLOATS_PER_BEAM_SEGMENT } from "@vector-display/core";
|
|
2
|
+
import { BEAM_FRAGMENT_SHADER, BEAM_VERTEX_SHADER } from "./beamShaders.js";
|
|
3
|
+
import { createShaderProgram } from "./createShaderProgram.js";
|
|
4
|
+
export const DEFAULT_LINE_STYLE = {
|
|
5
|
+
beamWidth: 1.5,
|
|
6
|
+
glowRadius: 4,
|
|
7
|
+
glowStrength: 0.25,
|
|
8
|
+
endpointBrightness: 0.6,
|
|
9
|
+
jointOverlap: 1,
|
|
10
|
+
};
|
|
11
|
+
const BYTES_PER_FLOAT = 4;
|
|
12
|
+
// Matches the packed layout documented on FLOATS_PER_BEAM_SEGMENT in @vector-display/core.
|
|
13
|
+
const SEGMENT_ATTRIBUTES = [
|
|
14
|
+
{ location: 0, size: 4, floatOffset: 0 },
|
|
15
|
+
{ location: 1, size: 4, floatOffset: 4 },
|
|
16
|
+
{ location: 2, size: 4, floatOffset: 8 },
|
|
17
|
+
{ location: 3, size: 2, floatOffset: 12 },
|
|
18
|
+
];
|
|
19
|
+
const GLOW_EXTENT_IN_RADII = 3;
|
|
20
|
+
export class WebGLVectorRenderer {
|
|
21
|
+
gl;
|
|
22
|
+
program;
|
|
23
|
+
uniforms;
|
|
24
|
+
vertexArray;
|
|
25
|
+
segmentBuffer;
|
|
26
|
+
segmentBufferCapacityBytes = 0;
|
|
27
|
+
worldSize;
|
|
28
|
+
lineStyle = DEFAULT_LINE_STYLE;
|
|
29
|
+
constructor(gl, worldSize) {
|
|
30
|
+
this.gl = gl;
|
|
31
|
+
this.worldSize = worldSize;
|
|
32
|
+
this.program = createShaderProgram(gl, {
|
|
33
|
+
vertexSource: BEAM_VERTEX_SHADER,
|
|
34
|
+
fragmentSource: BEAM_FRAGMENT_SHADER,
|
|
35
|
+
});
|
|
36
|
+
this.uniforms = this.findUniformLocations();
|
|
37
|
+
this.segmentBuffer = gl.createBuffer();
|
|
38
|
+
this.vertexArray = this.createSegmentVertexArray();
|
|
39
|
+
}
|
|
40
|
+
static fromCanvas(canvas, worldSize) {
|
|
41
|
+
const gl = canvas.getContext("webgl2", {
|
|
42
|
+
alpha: false,
|
|
43
|
+
antialias: false,
|
|
44
|
+
depth: false,
|
|
45
|
+
premultipliedAlpha: false,
|
|
46
|
+
});
|
|
47
|
+
if (!gl)
|
|
48
|
+
throw new Error("WebGL2 is not available in this browser");
|
|
49
|
+
return new WebGLVectorRenderer(gl, worldSize);
|
|
50
|
+
}
|
|
51
|
+
setWorldSize(worldSize) {
|
|
52
|
+
this.worldSize = worldSize;
|
|
53
|
+
}
|
|
54
|
+
setLineStyle(lineStyle) {
|
|
55
|
+
this.lineStyle = { ...this.lineStyle, ...lineStyle };
|
|
56
|
+
}
|
|
57
|
+
clear() {
|
|
58
|
+
const gl = this.gl;
|
|
59
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
60
|
+
gl.clearColor(0, 0, 0, 1);
|
|
61
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
62
|
+
}
|
|
63
|
+
// Draws additively into the currently bound framebuffer, which must match the drawing buffer size.
|
|
64
|
+
drawDisplayList(displayList) {
|
|
65
|
+
const segmentCount = displayList.segmentCount;
|
|
66
|
+
if (segmentCount === 0)
|
|
67
|
+
return;
|
|
68
|
+
const gl = this.gl;
|
|
69
|
+
this.uploadSegmentData(displayList.getSegmentData());
|
|
70
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
71
|
+
gl.enable(gl.BLEND);
|
|
72
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
73
|
+
gl.blendFunc(gl.ONE, gl.ONE);
|
|
74
|
+
gl.useProgram(this.program);
|
|
75
|
+
this.applyUniforms();
|
|
76
|
+
gl.bindVertexArray(this.vertexArray);
|
|
77
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, segmentCount);
|
|
78
|
+
gl.bindVertexArray(null);
|
|
79
|
+
}
|
|
80
|
+
dispose() {
|
|
81
|
+
this.gl.deleteBuffer(this.segmentBuffer);
|
|
82
|
+
this.gl.deleteVertexArray(this.vertexArray);
|
|
83
|
+
this.gl.deleteProgram(this.program);
|
|
84
|
+
}
|
|
85
|
+
findUniformLocations() {
|
|
86
|
+
const gl = this.gl;
|
|
87
|
+
const program = this.program;
|
|
88
|
+
return {
|
|
89
|
+
worldSize: gl.getUniformLocation(program, "u_worldSize"),
|
|
90
|
+
targetSize: gl.getUniformLocation(program, "u_targetSize"),
|
|
91
|
+
quadRadius: gl.getUniformLocation(program, "u_quadRadius"),
|
|
92
|
+
beamHalfWidth: gl.getUniformLocation(program, "u_beamHalfWidth"),
|
|
93
|
+
glowRadius: gl.getUniformLocation(program, "u_glowRadius"),
|
|
94
|
+
glowStrength: gl.getUniformLocation(program, "u_glowStrength"),
|
|
95
|
+
endpointBrightness: gl.getUniformLocation(program, "u_endpointBrightness"),
|
|
96
|
+
jointOverlap: gl.getUniformLocation(program, "u_jointOverlap"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
createSegmentVertexArray() {
|
|
100
|
+
const gl = this.gl;
|
|
101
|
+
const vertexArray = gl.createVertexArray();
|
|
102
|
+
const strideBytes = FLOATS_PER_BEAM_SEGMENT * BYTES_PER_FLOAT;
|
|
103
|
+
gl.bindVertexArray(vertexArray);
|
|
104
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.segmentBuffer);
|
|
105
|
+
for (const attribute of SEGMENT_ATTRIBUTES) {
|
|
106
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
107
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, gl.FLOAT, false, strideBytes, attribute.floatOffset * BYTES_PER_FLOAT);
|
|
108
|
+
gl.vertexAttribDivisor(attribute.location, 1);
|
|
109
|
+
}
|
|
110
|
+
gl.bindVertexArray(null);
|
|
111
|
+
return vertexArray;
|
|
112
|
+
}
|
|
113
|
+
uploadSegmentData(segmentData) {
|
|
114
|
+
const gl = this.gl;
|
|
115
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.segmentBuffer);
|
|
116
|
+
if (segmentData.byteLength > this.segmentBufferCapacityBytes) {
|
|
117
|
+
this.segmentBufferCapacityBytes = segmentData.byteLength * 2;
|
|
118
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.segmentBufferCapacityBytes, gl.DYNAMIC_DRAW);
|
|
119
|
+
}
|
|
120
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, segmentData);
|
|
121
|
+
}
|
|
122
|
+
applyUniforms() {
|
|
123
|
+
const gl = this.gl;
|
|
124
|
+
const { beamWidth, glowRadius, glowStrength, endpointBrightness, jointOverlap } = this.lineStyle;
|
|
125
|
+
const beamHalfWidth = beamWidth / 2;
|
|
126
|
+
gl.uniform2f(this.uniforms.worldSize, this.worldSize.width, this.worldSize.height);
|
|
127
|
+
gl.uniform2f(this.uniforms.targetSize, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
128
|
+
gl.uniform1f(this.uniforms.quadRadius, beamHalfWidth + glowRadius * GLOW_EXTENT_IN_RADII + 1);
|
|
129
|
+
gl.uniform1f(this.uniforms.beamHalfWidth, beamHalfWidth);
|
|
130
|
+
gl.uniform1f(this.uniforms.glowRadius, glowRadius);
|
|
131
|
+
gl.uniform1f(this.uniforms.glowStrength, glowStrength);
|
|
132
|
+
gl.uniform1f(this.uniforms.endpointBrightness, endpointBrightness);
|
|
133
|
+
gl.uniform1f(this.uniforms.jointOverlap, jointOverlap);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export default WebGLVectorRenderer;
|
|
137
|
+
//# sourceMappingURL=WebGLVectorRenderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebGLVectorRenderer.js","sourceRoot":"","sources":["../src/WebGLVectorRenderer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const BEAM_VERTEX_SHADER = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec4 a_segment;\nlayout(location = 1) in vec4 a_neighborPoints;\nlayout(location = 2) in vec4 a_colorIntensity;\nlayout(location = 3) in vec2 a_neighborFlags;\n\nuniform vec2 u_worldSize;\nuniform vec2 u_targetSize;\nuniform float u_quadRadius;\n\nflat out vec2 v_startPixel;\nflat out vec2 v_endPixel;\nflat out vec2 v_previousStartPixel;\nflat out vec2 v_nextEndPixel;\nflat out vec2 v_neighborFlags;\nflat out vec4 v_colorIntensity;\n\nvec2 worldToPixel(vec2 worldPosition) {\n vec2 normalized = worldPosition / u_worldSize;\n return vec2(normalized.x, 1.0 - normalized.y) * u_targetSize;\n}\n\nvoid main() {\n vec2 startPixel = worldToPixel(a_segment.xy);\n vec2 endPixel = worldToPixel(a_segment.zw);\n vec2 direction = endPixel - startPixel;\n float segmentLength = length(direction);\n vec2 tangent = segmentLength > 0.0001 ? direction / segmentLength : vec2(1.0, 0.0);\n vec2 normal = vec2(-tangent.y, tangent.x);\n\n // Triangle strip corners: vertex 0..3 -> (along, across) = (0,-1), (0,1), (1,-1), (1,1).\n float along = float(gl_VertexID / 2);\n float across = float(gl_VertexID % 2) * 2.0 - 1.0;\n vec2 cornerPixel = mix(startPixel, endPixel, along)\n + tangent * (along * 2.0 - 1.0) * u_quadRadius\n + normal * across * u_quadRadius;\n\n v_startPixel = startPixel;\n v_endPixel = endPixel;\n v_previousStartPixel = worldToPixel(a_neighborPoints.xy);\n v_nextEndPixel = worldToPixel(a_neighborPoints.zw);\n v_neighborFlags = a_neighborFlags;\n v_colorIntensity = a_colorIntensity;\n gl_Position = vec4(cornerPixel / u_targetSize * 2.0 - 1.0, 0.0, 1.0);\n}\n";
|
|
2
|
+
export declare const BEAM_FRAGMENT_SHADER = "#version 300 es\nprecision highp float;\n\nuniform float u_beamHalfWidth;\nuniform float u_glowRadius;\nuniform float u_glowStrength;\nuniform float u_endpointBrightness;\nuniform float u_jointOverlap;\n\nflat in vec2 v_startPixel;\nflat in vec2 v_endPixel;\nflat in vec2 v_previousStartPixel;\nflat in vec2 v_nextEndPixel;\nflat in vec2 v_neighborFlags;\nflat in vec4 v_colorIntensity;\n\nout vec4 outColor;\n\nfloat distanceToSegment(vec2 point, vec2 segmentStart, vec2 segmentEnd) {\n vec2 startToPoint = point - segmentStart;\n vec2 startToEnd = segmentEnd - segmentStart;\n float lengthSquared = max(dot(startToEnd, startToEnd), 0.000001);\n float projection = clamp(dot(startToPoint, startToEnd) / lengthSquared, 0.0, 1.0);\n return length(startToPoint - startToEnd * projection);\n}\n\nfloat gaussian(float distanceFromCenter, float radius) {\n return exp(-(distanceFromCenter * distanceFromCenter) / (2.0 * radius * radius));\n}\n\n// Near a joint, both connected segments cover the same pixels. The closer segment owns each pixel;\n// the other contributes only u_jointOverlap (1 = plain additive overlap, 0 = seamless joint).\n// Ties go to the previous segment so exactly one of the pair owns every pixel.\nfloat jointOwnership(vec2 pixel, float distanceFromBeam) {\n bool previousIsCloser = v_neighborFlags.x > 0.5\n && distanceToSegment(pixel, v_previousStartPixel, v_startPixel) <= distanceFromBeam;\n bool nextIsCloser = v_neighborFlags.y > 0.5\n && distanceToSegment(pixel, v_endPixel, v_nextEndPixel) < distanceFromBeam;\n return (previousIsCloser || nextIsCloser) ? u_jointOverlap : 1.0;\n}\n\nvoid main() {\n vec2 pixel = gl_FragCoord.xy;\n float distanceFromBeam = distanceToSegment(pixel, v_startPixel, v_endPixel);\n float coreCoverage = 1.0 - smoothstep(u_beamHalfWidth - 0.5, u_beamHalfWidth + 0.5, distanceFromBeam);\n float glow = u_glowStrength * gaussian(distanceFromBeam, u_glowRadius);\n\n // The beam dwells at segment endpoints, so real vector monitors drew vertices and dots brighter.\n float distanceFromEndpoint = min(distance(pixel, v_startPixel), distance(pixel, v_endPixel));\n float endpointDwell = u_endpointBrightness * gaussian(distanceFromEndpoint, u_beamHalfWidth * 2.0 + 0.5);\n\n float brightness = (coreCoverage + glow + endpointDwell) * v_colorIntensity.a * jointOwnership(pixel, distanceFromBeam);\n outColor = vec4(v_colorIntensity.rgb * brightness, 1.0);\n}\n";
|
|
3
|
+
//# sourceMappingURL=beamShaders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beamShaders.d.ts","sourceRoot":"","sources":["../src/beamShaders.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,kBAAkB,irDA+C9B,CAAC;AAEF,eAAO,MAAM,oBAAoB,m7EAsDhC,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Each beam segment is one instance: a quad around the segment, expanded in pixel space.
|
|
2
|
+
export const BEAM_VERTEX_SHADER = `#version 300 es
|
|
3
|
+
precision highp float;
|
|
4
|
+
|
|
5
|
+
layout(location = 0) in vec4 a_segment;
|
|
6
|
+
layout(location = 1) in vec4 a_neighborPoints;
|
|
7
|
+
layout(location = 2) in vec4 a_colorIntensity;
|
|
8
|
+
layout(location = 3) in vec2 a_neighborFlags;
|
|
9
|
+
|
|
10
|
+
uniform vec2 u_worldSize;
|
|
11
|
+
uniform vec2 u_targetSize;
|
|
12
|
+
uniform float u_quadRadius;
|
|
13
|
+
|
|
14
|
+
flat out vec2 v_startPixel;
|
|
15
|
+
flat out vec2 v_endPixel;
|
|
16
|
+
flat out vec2 v_previousStartPixel;
|
|
17
|
+
flat out vec2 v_nextEndPixel;
|
|
18
|
+
flat out vec2 v_neighborFlags;
|
|
19
|
+
flat out vec4 v_colorIntensity;
|
|
20
|
+
|
|
21
|
+
vec2 worldToPixel(vec2 worldPosition) {
|
|
22
|
+
vec2 normalized = worldPosition / u_worldSize;
|
|
23
|
+
return vec2(normalized.x, 1.0 - normalized.y) * u_targetSize;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
void main() {
|
|
27
|
+
vec2 startPixel = worldToPixel(a_segment.xy);
|
|
28
|
+
vec2 endPixel = worldToPixel(a_segment.zw);
|
|
29
|
+
vec2 direction = endPixel - startPixel;
|
|
30
|
+
float segmentLength = length(direction);
|
|
31
|
+
vec2 tangent = segmentLength > 0.0001 ? direction / segmentLength : vec2(1.0, 0.0);
|
|
32
|
+
vec2 normal = vec2(-tangent.y, tangent.x);
|
|
33
|
+
|
|
34
|
+
// Triangle strip corners: vertex 0..3 -> (along, across) = (0,-1), (0,1), (1,-1), (1,1).
|
|
35
|
+
float along = float(gl_VertexID / 2);
|
|
36
|
+
float across = float(gl_VertexID % 2) * 2.0 - 1.0;
|
|
37
|
+
vec2 cornerPixel = mix(startPixel, endPixel, along)
|
|
38
|
+
+ tangent * (along * 2.0 - 1.0) * u_quadRadius
|
|
39
|
+
+ normal * across * u_quadRadius;
|
|
40
|
+
|
|
41
|
+
v_startPixel = startPixel;
|
|
42
|
+
v_endPixel = endPixel;
|
|
43
|
+
v_previousStartPixel = worldToPixel(a_neighborPoints.xy);
|
|
44
|
+
v_nextEndPixel = worldToPixel(a_neighborPoints.zw);
|
|
45
|
+
v_neighborFlags = a_neighborFlags;
|
|
46
|
+
v_colorIntensity = a_colorIntensity;
|
|
47
|
+
gl_Position = vec4(cornerPixel / u_targetSize * 2.0 - 1.0, 0.0, 1.0);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
export const BEAM_FRAGMENT_SHADER = `#version 300 es
|
|
51
|
+
precision highp float;
|
|
52
|
+
|
|
53
|
+
uniform float u_beamHalfWidth;
|
|
54
|
+
uniform float u_glowRadius;
|
|
55
|
+
uniform float u_glowStrength;
|
|
56
|
+
uniform float u_endpointBrightness;
|
|
57
|
+
uniform float u_jointOverlap;
|
|
58
|
+
|
|
59
|
+
flat in vec2 v_startPixel;
|
|
60
|
+
flat in vec2 v_endPixel;
|
|
61
|
+
flat in vec2 v_previousStartPixel;
|
|
62
|
+
flat in vec2 v_nextEndPixel;
|
|
63
|
+
flat in vec2 v_neighborFlags;
|
|
64
|
+
flat in vec4 v_colorIntensity;
|
|
65
|
+
|
|
66
|
+
out vec4 outColor;
|
|
67
|
+
|
|
68
|
+
float distanceToSegment(vec2 point, vec2 segmentStart, vec2 segmentEnd) {
|
|
69
|
+
vec2 startToPoint = point - segmentStart;
|
|
70
|
+
vec2 startToEnd = segmentEnd - segmentStart;
|
|
71
|
+
float lengthSquared = max(dot(startToEnd, startToEnd), 0.000001);
|
|
72
|
+
float projection = clamp(dot(startToPoint, startToEnd) / lengthSquared, 0.0, 1.0);
|
|
73
|
+
return length(startToPoint - startToEnd * projection);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
float gaussian(float distanceFromCenter, float radius) {
|
|
77
|
+
return exp(-(distanceFromCenter * distanceFromCenter) / (2.0 * radius * radius));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Near a joint, both connected segments cover the same pixels. The closer segment owns each pixel;
|
|
81
|
+
// the other contributes only u_jointOverlap (1 = plain additive overlap, 0 = seamless joint).
|
|
82
|
+
// Ties go to the previous segment so exactly one of the pair owns every pixel.
|
|
83
|
+
float jointOwnership(vec2 pixel, float distanceFromBeam) {
|
|
84
|
+
bool previousIsCloser = v_neighborFlags.x > 0.5
|
|
85
|
+
&& distanceToSegment(pixel, v_previousStartPixel, v_startPixel) <= distanceFromBeam;
|
|
86
|
+
bool nextIsCloser = v_neighborFlags.y > 0.5
|
|
87
|
+
&& distanceToSegment(pixel, v_endPixel, v_nextEndPixel) < distanceFromBeam;
|
|
88
|
+
return (previousIsCloser || nextIsCloser) ? u_jointOverlap : 1.0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
void main() {
|
|
92
|
+
vec2 pixel = gl_FragCoord.xy;
|
|
93
|
+
float distanceFromBeam = distanceToSegment(pixel, v_startPixel, v_endPixel);
|
|
94
|
+
float coreCoverage = 1.0 - smoothstep(u_beamHalfWidth - 0.5, u_beamHalfWidth + 0.5, distanceFromBeam);
|
|
95
|
+
float glow = u_glowStrength * gaussian(distanceFromBeam, u_glowRadius);
|
|
96
|
+
|
|
97
|
+
// The beam dwells at segment endpoints, so real vector monitors drew vertices and dots brighter.
|
|
98
|
+
float distanceFromEndpoint = min(distance(pixel, v_startPixel), distance(pixel, v_endPixel));
|
|
99
|
+
float endpointDwell = u_endpointBrightness * gaussian(distanceFromEndpoint, u_beamHalfWidth * 2.0 + 0.5);
|
|
100
|
+
|
|
101
|
+
float brightness = (coreCoverage + glow + endpointDwell) * v_colorIntensity.a * jointOwnership(pixel, distanceFromBeam);
|
|
102
|
+
outColor = vec4(v_colorIntensity.rgb * brightness, 1.0);
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
//# sourceMappingURL=beamShaders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beamShaders.js","sourceRoot":"","sources":["../src/beamShaders.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CjC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDnC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createShaderProgram.d.ts","sourceRoot":"","sources":["../src/createShaderProgram.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACnC;AAOD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,sBAAsB,EAAE,OAAO,EAAE,aAAa,GAAG,YAAY,CAepG"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function createShaderProgram(gl, sources) {
|
|
2
|
+
const vertexShader = compileShader(gl, { shaderType: gl.VERTEX_SHADER, source: sources.vertexSource });
|
|
3
|
+
const fragmentShader = compileShader(gl, { shaderType: gl.FRAGMENT_SHADER, source: sources.fragmentSource });
|
|
4
|
+
const program = gl.createProgram();
|
|
5
|
+
gl.attachShader(program, vertexShader);
|
|
6
|
+
gl.attachShader(program, fragmentShader);
|
|
7
|
+
gl.linkProgram(program);
|
|
8
|
+
gl.deleteShader(vertexShader);
|
|
9
|
+
gl.deleteShader(fragmentShader);
|
|
10
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
11
|
+
const log = gl.getProgramInfoLog(program);
|
|
12
|
+
gl.deleteProgram(program);
|
|
13
|
+
throw new Error(`Failed to link shader program: ${log}`);
|
|
14
|
+
}
|
|
15
|
+
return program;
|
|
16
|
+
}
|
|
17
|
+
function compileShader(gl, stage) {
|
|
18
|
+
const shader = gl.createShader(stage.shaderType);
|
|
19
|
+
if (!shader)
|
|
20
|
+
throw new Error("Failed to create shader");
|
|
21
|
+
gl.shaderSource(shader, stage.source);
|
|
22
|
+
gl.compileShader(shader);
|
|
23
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
24
|
+
const log = gl.getShaderInfoLog(shader);
|
|
25
|
+
gl.deleteShader(shader);
|
|
26
|
+
throw new Error(`Failed to compile shader: ${log}`);
|
|
27
|
+
}
|
|
28
|
+
return shader;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=createShaderProgram.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createShaderProgram.js","sourceRoot":"","sources":["../src/createShaderProgram.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,mBAAmB,CAAC,EAA0B,EAAE,OAAsB;IAClF,MAAM,YAAY,GAAG,aAAa,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IACvG,MAAM,cAAc,GAAG,aAAa,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC7G,MAAM,OAAO,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC;IACnC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACvC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACzC,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACxB,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAC9B,EAAE,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC1C,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,kCAAkC,GAAG,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,SAAS,aAAa,CAAC,EAA0B,EAAE,KAAkB;IACjE,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACjD,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACxD,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACzB,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC;QACpD,MAAM,GAAG,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACxC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { WebGLVectorRenderer, DEFAULT_LINE_STYLE } from "./WebGLVectorRenderer.js";
|
|
2
|
+
export type { LineStyle, WorldSize } from "./WebGLVectorRenderer.js";
|
|
3
|
+
export { createShaderProgram } from "./createShaderProgram.js";
|
|
4
|
+
export type { ShaderSources } from "./createShaderProgram.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnF,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vector-display/webgl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WebGL2 renderer that draws vector-display beam strokes in a single batched draw call.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Rick Segrest",
|
|
8
|
+
"email": "rsegrest77@gmail.com",
|
|
9
|
+
"url": "https://www.ricksegrest.com"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/rsegrest/vector-display.git",
|
|
14
|
+
"directory": "packages/webgl"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/rsegrest/vector-display/tree/main/packages/webgl#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/rsegrest/vector-display/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"@vector-display/source": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"prepublishOnly": "tsc -b"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"!src/**/*.test.ts"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@vector-display/core": "0.1.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { FLOATS_PER_BEAM_SEGMENT, type DisplayList } from "@vector-display/core";
|
|
2
|
+
import { BEAM_FRAGMENT_SHADER, BEAM_VERTEX_SHADER } from "./beamShaders.js";
|
|
3
|
+
import { createShaderProgram } from "./createShaderProgram.js";
|
|
4
|
+
|
|
5
|
+
// All sizes are in framebuffer pixels (multiply CSS pixels by devicePixelRatio).
|
|
6
|
+
export interface LineStyle {
|
|
7
|
+
readonly beamWidth: number;
|
|
8
|
+
readonly glowRadius: number;
|
|
9
|
+
readonly glowStrength: number;
|
|
10
|
+
// Extra brightness where the beam dwells at segment endpoints; 0 removes the highlight.
|
|
11
|
+
readonly endpointBrightness: number;
|
|
12
|
+
// How much connected segments add up where they meet: 1 = additive overlap (brighter joints), 0 = seamless.
|
|
13
|
+
readonly jointOverlap: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WorldSize {
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_LINE_STYLE: LineStyle = {
|
|
22
|
+
beamWidth: 1.5,
|
|
23
|
+
glowRadius: 4,
|
|
24
|
+
glowStrength: 0.25,
|
|
25
|
+
endpointBrightness: 0.6,
|
|
26
|
+
jointOverlap: 1,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const BYTES_PER_FLOAT = 4;
|
|
30
|
+
|
|
31
|
+
// Matches the packed layout documented on FLOATS_PER_BEAM_SEGMENT in @vector-display/core.
|
|
32
|
+
const SEGMENT_ATTRIBUTES = [
|
|
33
|
+
{ location: 0, size: 4, floatOffset: 0 },
|
|
34
|
+
{ location: 1, size: 4, floatOffset: 4 },
|
|
35
|
+
{ location: 2, size: 4, floatOffset: 8 },
|
|
36
|
+
{ location: 3, size: 2, floatOffset: 12 },
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
const GLOW_EXTENT_IN_RADII = 3;
|
|
40
|
+
|
|
41
|
+
interface BeamUniformLocations {
|
|
42
|
+
readonly worldSize: WebGLUniformLocation | null;
|
|
43
|
+
readonly targetSize: WebGLUniformLocation | null;
|
|
44
|
+
readonly quadRadius: WebGLUniformLocation | null;
|
|
45
|
+
readonly beamHalfWidth: WebGLUniformLocation | null;
|
|
46
|
+
readonly glowRadius: WebGLUniformLocation | null;
|
|
47
|
+
readonly glowStrength: WebGLUniformLocation | null;
|
|
48
|
+
readonly endpointBrightness: WebGLUniformLocation | null;
|
|
49
|
+
readonly jointOverlap: WebGLUniformLocation | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class WebGLVectorRenderer {
|
|
53
|
+
public readonly gl: WebGL2RenderingContext;
|
|
54
|
+
private readonly program: WebGLProgram;
|
|
55
|
+
private readonly uniforms: BeamUniformLocations;
|
|
56
|
+
private readonly vertexArray: WebGLVertexArrayObject;
|
|
57
|
+
private readonly segmentBuffer: WebGLBuffer;
|
|
58
|
+
private segmentBufferCapacityBytes = 0;
|
|
59
|
+
private worldSize: WorldSize;
|
|
60
|
+
private lineStyle: LineStyle = DEFAULT_LINE_STYLE;
|
|
61
|
+
|
|
62
|
+
constructor(gl: WebGL2RenderingContext, worldSize: WorldSize) {
|
|
63
|
+
this.gl = gl;
|
|
64
|
+
this.worldSize = worldSize;
|
|
65
|
+
this.program = createShaderProgram(gl, {
|
|
66
|
+
vertexSource: BEAM_VERTEX_SHADER,
|
|
67
|
+
fragmentSource: BEAM_FRAGMENT_SHADER,
|
|
68
|
+
});
|
|
69
|
+
this.uniforms = this.findUniformLocations();
|
|
70
|
+
this.segmentBuffer = gl.createBuffer();
|
|
71
|
+
this.vertexArray = this.createSegmentVertexArray();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public static fromCanvas(canvas: HTMLCanvasElement, worldSize: WorldSize): WebGLVectorRenderer {
|
|
75
|
+
const gl = canvas.getContext("webgl2", {
|
|
76
|
+
alpha: false,
|
|
77
|
+
antialias: false,
|
|
78
|
+
depth: false,
|
|
79
|
+
premultipliedAlpha: false,
|
|
80
|
+
});
|
|
81
|
+
if (!gl) throw new Error("WebGL2 is not available in this browser");
|
|
82
|
+
return new WebGLVectorRenderer(gl, worldSize);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public setWorldSize(worldSize: WorldSize): void {
|
|
86
|
+
this.worldSize = worldSize;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public setLineStyle(lineStyle: Partial<LineStyle>): void {
|
|
90
|
+
this.lineStyle = { ...this.lineStyle, ...lineStyle };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public clear(): void {
|
|
94
|
+
const gl = this.gl;
|
|
95
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
96
|
+
gl.clearColor(0, 0, 0, 1);
|
|
97
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Draws additively into the currently bound framebuffer, which must match the drawing buffer size.
|
|
101
|
+
public drawDisplayList(displayList: DisplayList): void {
|
|
102
|
+
const segmentCount = displayList.segmentCount;
|
|
103
|
+
if (segmentCount === 0) return;
|
|
104
|
+
const gl = this.gl;
|
|
105
|
+
this.uploadSegmentData(displayList.getSegmentData());
|
|
106
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
107
|
+
gl.enable(gl.BLEND);
|
|
108
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
109
|
+
gl.blendFunc(gl.ONE, gl.ONE);
|
|
110
|
+
gl.useProgram(this.program);
|
|
111
|
+
this.applyUniforms();
|
|
112
|
+
gl.bindVertexArray(this.vertexArray);
|
|
113
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, segmentCount);
|
|
114
|
+
gl.bindVertexArray(null);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public dispose(): void {
|
|
118
|
+
this.gl.deleteBuffer(this.segmentBuffer);
|
|
119
|
+
this.gl.deleteVertexArray(this.vertexArray);
|
|
120
|
+
this.gl.deleteProgram(this.program);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private findUniformLocations(): BeamUniformLocations {
|
|
124
|
+
const gl = this.gl;
|
|
125
|
+
const program = this.program;
|
|
126
|
+
return {
|
|
127
|
+
worldSize: gl.getUniformLocation(program, "u_worldSize"),
|
|
128
|
+
targetSize: gl.getUniformLocation(program, "u_targetSize"),
|
|
129
|
+
quadRadius: gl.getUniformLocation(program, "u_quadRadius"),
|
|
130
|
+
beamHalfWidth: gl.getUniformLocation(program, "u_beamHalfWidth"),
|
|
131
|
+
glowRadius: gl.getUniformLocation(program, "u_glowRadius"),
|
|
132
|
+
glowStrength: gl.getUniformLocation(program, "u_glowStrength"),
|
|
133
|
+
endpointBrightness: gl.getUniformLocation(program, "u_endpointBrightness"),
|
|
134
|
+
jointOverlap: gl.getUniformLocation(program, "u_jointOverlap"),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private createSegmentVertexArray(): WebGLVertexArrayObject {
|
|
139
|
+
const gl = this.gl;
|
|
140
|
+
const vertexArray = gl.createVertexArray();
|
|
141
|
+
const strideBytes = FLOATS_PER_BEAM_SEGMENT * BYTES_PER_FLOAT;
|
|
142
|
+
gl.bindVertexArray(vertexArray);
|
|
143
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.segmentBuffer);
|
|
144
|
+
for (const attribute of SEGMENT_ATTRIBUTES) {
|
|
145
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
146
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, gl.FLOAT, false, strideBytes, attribute.floatOffset * BYTES_PER_FLOAT);
|
|
147
|
+
gl.vertexAttribDivisor(attribute.location, 1);
|
|
148
|
+
}
|
|
149
|
+
gl.bindVertexArray(null);
|
|
150
|
+
return vertexArray;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private uploadSegmentData(segmentData: Float32Array): void {
|
|
154
|
+
const gl = this.gl;
|
|
155
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.segmentBuffer);
|
|
156
|
+
if (segmentData.byteLength > this.segmentBufferCapacityBytes) {
|
|
157
|
+
this.segmentBufferCapacityBytes = segmentData.byteLength * 2;
|
|
158
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.segmentBufferCapacityBytes, gl.DYNAMIC_DRAW);
|
|
159
|
+
}
|
|
160
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, segmentData);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private applyUniforms(): void {
|
|
164
|
+
const gl = this.gl;
|
|
165
|
+
const { beamWidth, glowRadius, glowStrength, endpointBrightness, jointOverlap } = this.lineStyle;
|
|
166
|
+
const beamHalfWidth = beamWidth / 2;
|
|
167
|
+
gl.uniform2f(this.uniforms.worldSize, this.worldSize.width, this.worldSize.height);
|
|
168
|
+
gl.uniform2f(this.uniforms.targetSize, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
169
|
+
gl.uniform1f(this.uniforms.quadRadius, beamHalfWidth + glowRadius * GLOW_EXTENT_IN_RADII + 1);
|
|
170
|
+
gl.uniform1f(this.uniforms.beamHalfWidth, beamHalfWidth);
|
|
171
|
+
gl.uniform1f(this.uniforms.glowRadius, glowRadius);
|
|
172
|
+
gl.uniform1f(this.uniforms.glowStrength, glowStrength);
|
|
173
|
+
gl.uniform1f(this.uniforms.endpointBrightness, endpointBrightness);
|
|
174
|
+
gl.uniform1f(this.uniforms.jointOverlap, jointOverlap);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default WebGLVectorRenderer;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Each beam segment is one instance: a quad around the segment, expanded in pixel space.
|
|
2
|
+
export const BEAM_VERTEX_SHADER = `#version 300 es
|
|
3
|
+
precision highp float;
|
|
4
|
+
|
|
5
|
+
layout(location = 0) in vec4 a_segment;
|
|
6
|
+
layout(location = 1) in vec4 a_neighborPoints;
|
|
7
|
+
layout(location = 2) in vec4 a_colorIntensity;
|
|
8
|
+
layout(location = 3) in vec2 a_neighborFlags;
|
|
9
|
+
|
|
10
|
+
uniform vec2 u_worldSize;
|
|
11
|
+
uniform vec2 u_targetSize;
|
|
12
|
+
uniform float u_quadRadius;
|
|
13
|
+
|
|
14
|
+
flat out vec2 v_startPixel;
|
|
15
|
+
flat out vec2 v_endPixel;
|
|
16
|
+
flat out vec2 v_previousStartPixel;
|
|
17
|
+
flat out vec2 v_nextEndPixel;
|
|
18
|
+
flat out vec2 v_neighborFlags;
|
|
19
|
+
flat out vec4 v_colorIntensity;
|
|
20
|
+
|
|
21
|
+
vec2 worldToPixel(vec2 worldPosition) {
|
|
22
|
+
vec2 normalized = worldPosition / u_worldSize;
|
|
23
|
+
return vec2(normalized.x, 1.0 - normalized.y) * u_targetSize;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
void main() {
|
|
27
|
+
vec2 startPixel = worldToPixel(a_segment.xy);
|
|
28
|
+
vec2 endPixel = worldToPixel(a_segment.zw);
|
|
29
|
+
vec2 direction = endPixel - startPixel;
|
|
30
|
+
float segmentLength = length(direction);
|
|
31
|
+
vec2 tangent = segmentLength > 0.0001 ? direction / segmentLength : vec2(1.0, 0.0);
|
|
32
|
+
vec2 normal = vec2(-tangent.y, tangent.x);
|
|
33
|
+
|
|
34
|
+
// Triangle strip corners: vertex 0..3 -> (along, across) = (0,-1), (0,1), (1,-1), (1,1).
|
|
35
|
+
float along = float(gl_VertexID / 2);
|
|
36
|
+
float across = float(gl_VertexID % 2) * 2.0 - 1.0;
|
|
37
|
+
vec2 cornerPixel = mix(startPixel, endPixel, along)
|
|
38
|
+
+ tangent * (along * 2.0 - 1.0) * u_quadRadius
|
|
39
|
+
+ normal * across * u_quadRadius;
|
|
40
|
+
|
|
41
|
+
v_startPixel = startPixel;
|
|
42
|
+
v_endPixel = endPixel;
|
|
43
|
+
v_previousStartPixel = worldToPixel(a_neighborPoints.xy);
|
|
44
|
+
v_nextEndPixel = worldToPixel(a_neighborPoints.zw);
|
|
45
|
+
v_neighborFlags = a_neighborFlags;
|
|
46
|
+
v_colorIntensity = a_colorIntensity;
|
|
47
|
+
gl_Position = vec4(cornerPixel / u_targetSize * 2.0 - 1.0, 0.0, 1.0);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export const BEAM_FRAGMENT_SHADER = `#version 300 es
|
|
52
|
+
precision highp float;
|
|
53
|
+
|
|
54
|
+
uniform float u_beamHalfWidth;
|
|
55
|
+
uniform float u_glowRadius;
|
|
56
|
+
uniform float u_glowStrength;
|
|
57
|
+
uniform float u_endpointBrightness;
|
|
58
|
+
uniform float u_jointOverlap;
|
|
59
|
+
|
|
60
|
+
flat in vec2 v_startPixel;
|
|
61
|
+
flat in vec2 v_endPixel;
|
|
62
|
+
flat in vec2 v_previousStartPixel;
|
|
63
|
+
flat in vec2 v_nextEndPixel;
|
|
64
|
+
flat in vec2 v_neighborFlags;
|
|
65
|
+
flat in vec4 v_colorIntensity;
|
|
66
|
+
|
|
67
|
+
out vec4 outColor;
|
|
68
|
+
|
|
69
|
+
float distanceToSegment(vec2 point, vec2 segmentStart, vec2 segmentEnd) {
|
|
70
|
+
vec2 startToPoint = point - segmentStart;
|
|
71
|
+
vec2 startToEnd = segmentEnd - segmentStart;
|
|
72
|
+
float lengthSquared = max(dot(startToEnd, startToEnd), 0.000001);
|
|
73
|
+
float projection = clamp(dot(startToPoint, startToEnd) / lengthSquared, 0.0, 1.0);
|
|
74
|
+
return length(startToPoint - startToEnd * projection);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
float gaussian(float distanceFromCenter, float radius) {
|
|
78
|
+
return exp(-(distanceFromCenter * distanceFromCenter) / (2.0 * radius * radius));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Near a joint, both connected segments cover the same pixels. The closer segment owns each pixel;
|
|
82
|
+
// the other contributes only u_jointOverlap (1 = plain additive overlap, 0 = seamless joint).
|
|
83
|
+
// Ties go to the previous segment so exactly one of the pair owns every pixel.
|
|
84
|
+
float jointOwnership(vec2 pixel, float distanceFromBeam) {
|
|
85
|
+
bool previousIsCloser = v_neighborFlags.x > 0.5
|
|
86
|
+
&& distanceToSegment(pixel, v_previousStartPixel, v_startPixel) <= distanceFromBeam;
|
|
87
|
+
bool nextIsCloser = v_neighborFlags.y > 0.5
|
|
88
|
+
&& distanceToSegment(pixel, v_endPixel, v_nextEndPixel) < distanceFromBeam;
|
|
89
|
+
return (previousIsCloser || nextIsCloser) ? u_jointOverlap : 1.0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
void main() {
|
|
93
|
+
vec2 pixel = gl_FragCoord.xy;
|
|
94
|
+
float distanceFromBeam = distanceToSegment(pixel, v_startPixel, v_endPixel);
|
|
95
|
+
float coreCoverage = 1.0 - smoothstep(u_beamHalfWidth - 0.5, u_beamHalfWidth + 0.5, distanceFromBeam);
|
|
96
|
+
float glow = u_glowStrength * gaussian(distanceFromBeam, u_glowRadius);
|
|
97
|
+
|
|
98
|
+
// The beam dwells at segment endpoints, so real vector monitors drew vertices and dots brighter.
|
|
99
|
+
float distanceFromEndpoint = min(distance(pixel, v_startPixel), distance(pixel, v_endPixel));
|
|
100
|
+
float endpointDwell = u_endpointBrightness * gaussian(distanceFromEndpoint, u_beamHalfWidth * 2.0 + 0.5);
|
|
101
|
+
|
|
102
|
+
float brightness = (coreCoverage + glow + endpointDwell) * v_colorIntensity.a * jointOwnership(pixel, distanceFromBeam);
|
|
103
|
+
outColor = vec4(v_colorIntensity.rgb * brightness, 1.0);
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ShaderSources {
|
|
2
|
+
readonly vertexSource: string;
|
|
3
|
+
readonly fragmentSource: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ShaderStage {
|
|
7
|
+
readonly shaderType: GLenum;
|
|
8
|
+
readonly source: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createShaderProgram(gl: WebGL2RenderingContext, sources: ShaderSources): WebGLProgram {
|
|
12
|
+
const vertexShader = compileShader(gl, { shaderType: gl.VERTEX_SHADER, source: sources.vertexSource });
|
|
13
|
+
const fragmentShader = compileShader(gl, { shaderType: gl.FRAGMENT_SHADER, source: sources.fragmentSource });
|
|
14
|
+
const program = gl.createProgram();
|
|
15
|
+
gl.attachShader(program, vertexShader);
|
|
16
|
+
gl.attachShader(program, fragmentShader);
|
|
17
|
+
gl.linkProgram(program);
|
|
18
|
+
gl.deleteShader(vertexShader);
|
|
19
|
+
gl.deleteShader(fragmentShader);
|
|
20
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
21
|
+
const log = gl.getProgramInfoLog(program);
|
|
22
|
+
gl.deleteProgram(program);
|
|
23
|
+
throw new Error(`Failed to link shader program: ${log}`);
|
|
24
|
+
}
|
|
25
|
+
return program;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compileShader(gl: WebGL2RenderingContext, stage: ShaderStage): WebGLShader {
|
|
29
|
+
const shader = gl.createShader(stage.shaderType);
|
|
30
|
+
if (!shader) throw new Error("Failed to create shader");
|
|
31
|
+
gl.shaderSource(shader, stage.source);
|
|
32
|
+
gl.compileShader(shader);
|
|
33
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
34
|
+
const log = gl.getShaderInfoLog(shader);
|
|
35
|
+
gl.deleteShader(shader);
|
|
36
|
+
throw new Error(`Failed to compile shader: ${log}`);
|
|
37
|
+
}
|
|
38
|
+
return shader;
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { WebGLVectorRenderer, DEFAULT_LINE_STYLE } from "./WebGLVectorRenderer.js";
|
|
2
|
+
export type { LineStyle, WorldSize } from "./WebGLVectorRenderer.js";
|
|
3
|
+
export { createShaderProgram } from "./createShaderProgram.js";
|
|
4
|
+
export type { ShaderSources } from "./createShaderProgram.js";
|