Skip to content

Commit f832a73

Browse files
committed
New: Add labels and node images
1 parent a27afd2 commit f832a73

7 files changed

Lines changed: 650 additions & 55 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#version 300 es
2+
3+
precision highp float;
4+
5+
uniform sampler2D uAtlas;
6+
7+
in vec2 vAtlasUV;
8+
9+
out vec4 fragColor;
10+
11+
void main() {
12+
vec4 texel = texture(uAtlas, vAtlasUV);
13+
if (texel.a < 0.01) discard;
14+
fragColor = texel;
15+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#version 300 es
2+
3+
in vec2 aQuadPosition;
4+
5+
in vec2 aLabelCenter;
6+
in vec2 aLabelSize;
7+
in vec2 aLabelUV0;
8+
in vec2 aLabelUV1;
9+
10+
uniform vec2 uResolution;
11+
uniform vec2 uTranslation;
12+
uniform float uScale;
13+
uniform vec2 uOriginOffset;
14+
15+
out vec2 vAtlasUV;
16+
17+
void main() {
18+
vec2 worldPos = aLabelCenter + aQuadPosition * aLabelSize;
19+
vec2 screenPos = (worldPos + uOriginOffset) * uScale + uTranslation;
20+
vec2 clip = (screenPos / uResolution) * 2.0 - 1.0;
21+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
22+
23+
vec2 uv01 = aQuadPosition * 0.5 + 0.5;
24+
vAtlasUV = mix(aLabelUV0, aLabelUV1, uv01);
25+
}

src/renderer/webgl/shaders/node/node.frag

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ in float vNodeRadius;
1111
in vec2 vShadowOffset;
1212
in float vShadowBlur;
1313
flat in int vShapeType;
14+
in vec2 vImageUV0;
15+
in vec2 vImageUV1;
16+
in float vImageAspect;
17+
18+
uniform sampler2D uImageAtlas;
1419

1520
out vec4 fragColor;
1621

@@ -114,9 +119,24 @@ void main() {
114119
float aa = 0.02 * vNodeRadius;
115120
float nodeAlpha = 1.0 - smoothstep(-aa, 0.0, dist);
116121

122+
vec4 fillColor = vColor;
123+
if (vImageAspect > 0.0 && dist < 0.0) {
124+
vec2 uv01 = (vUV / vNodeRadius) * 0.5 + 0.5;
125+
if (vImageAspect > 1.0) {
126+
uv01.x = (uv01.x - 0.5) / vImageAspect + 0.5;
127+
} else {
128+
uv01.y = (uv01.y - 0.5) * vImageAspect + 0.5;
129+
}
130+
if (uv01.x >= 0.0 && uv01.x <= 1.0 && uv01.y >= 0.0 && uv01.y <= 1.0) {
131+
vec2 atlasUV = mix(vImageUV0, vImageUV1, uv01);
132+
vec4 imgTexel = texture(uImageAtlas, atlasUV);
133+
fillColor = mix(fillColor, vec4(imgTexel.rgb, 1.0), imgTexel.a);
134+
}
135+
}
136+
117137
float borderDist = shapeSDF(vUV, vBorderThreshold, vShapeType);
118138
float borderMix = smoothstep(-aa, aa, borderDist);
119-
vec4 nodeColor = mix(vColor, vBorderColor, borderMix);
139+
vec4 nodeColor = mix(fillColor, vBorderColor, borderMix);
120140
nodeColor.a *= nodeAlpha;
121141

122142
float finalAlpha = nodeColor.a + shadowAlpha * (1.0 - nodeColor.a);

src/renderer/webgl/shaders/node/node.vert

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ in float aShadowSize;
1414
in float aShadowOffsetX;
1515
in float aShadowOffsetY;
1616
in float aShapeType;
17+
in vec2 aImageUV0;
18+
in vec2 aImageUV1;
19+
in float aImageAspect;
1720

1821
uniform vec2 uResolution;
1922
uniform vec2 uTranslation;
@@ -29,12 +32,18 @@ out float vNodeRadius;
2932
out vec2 vShadowOffset;
3033
out float vShadowBlur;
3134
flat out int vShapeType;
35+
out vec2 vImageUV0;
36+
out vec2 vImageUV1;
37+
out float vImageAspect;
3238

3339
void main() {
3440
vShapeType = int(aShapeType + 0.5);
3541
vColor = aColor;
3642
vBorderColor = aBorderColor;
3743
vShadowColor = aShadowColor;
44+
vImageUV0 = aImageUV0;
45+
vImageUV1 = aImageUV1;
46+
vImageAspect = aImageAspect;
3847

3948
float totalRadius = aRadius + aShadowSize + abs(aShadowOffsetX) + abs(aShadowOffsetY);
4049

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
const ATLAS_WIDTH = 2048;
2+
const ATLAS_HEIGHT = 2048;
3+
const MAX_CELL_SIZE = 128;
4+
const PADDING = 2;
5+
6+
export interface ImageAtlasEntry {
7+
u0: number;
8+
v0: number;
9+
u1: number;
10+
v1: number;
11+
aspect: number;
12+
}
13+
14+
interface Shelf {
15+
y: number;
16+
height: number;
17+
x: number;
18+
}
19+
20+
interface PendingImage {
21+
image: HTMLImageElement;
22+
loaded: boolean;
23+
}
24+
25+
export class ImageAtlas {
26+
private _gl: WebGL2RenderingContext;
27+
private _canvas: HTMLCanvasElement;
28+
private _ctx: CanvasRenderingContext2D;
29+
private _texture: WebGLTexture | null = null;
30+
private _cache = new Map<string, ImageAtlasEntry>();
31+
private _pending = new Map<string, PendingImage>();
32+
private _shelves: Shelf[] = [];
33+
private _dirty = false;
34+
private _textureAllocated = false;
35+
36+
constructor(gl: WebGL2RenderingContext) {
37+
this._gl = gl;
38+
this._canvas = document.createElement('canvas');
39+
this._canvas.width = ATLAS_WIDTH;
40+
this._canvas.height = ATLAS_HEIGHT;
41+
this._ctx = this._canvas.getContext('2d', { willReadFrequently: false })!;
42+
this._texture = gl.createTexture();
43+
44+
gl.bindTexture(gl.TEXTURE_2D, this._texture);
45+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
46+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
47+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
48+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
49+
gl.bindTexture(gl.TEXTURE_2D, null);
50+
}
51+
52+
getOrCreate(url: string): ImageAtlasEntry | null {
53+
const cached = this._cache.get(url);
54+
if (cached) {
55+
return cached;
56+
}
57+
58+
const pending = this._pending.get(url);
59+
if (pending) {
60+
if (!pending.loaded) {
61+
return null;
62+
}
63+
return this._packImage(url, pending.image);
64+
}
65+
66+
const image = new Image();
67+
image.crossOrigin = 'anonymous';
68+
69+
const record: PendingImage = { image, loaded: false };
70+
this._pending.set(url, record);
71+
72+
image.onload = () => {
73+
record.loaded = true;
74+
};
75+
image.onerror = () => {
76+
this._pending.delete(url);
77+
};
78+
image.src = url;
79+
80+
return null;
81+
}
82+
83+
bind(unit: number): void {
84+
const gl = this._gl;
85+
gl.activeTexture(gl.TEXTURE0 + unit);
86+
gl.bindTexture(gl.TEXTURE_2D, this._texture);
87+
}
88+
89+
uploadIfDirty(): void {
90+
if (!this._dirty) {
91+
return;
92+
}
93+
94+
const gl = this._gl;
95+
gl.bindTexture(gl.TEXTURE_2D, this._texture);
96+
97+
if (!this._textureAllocated) {
98+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ATLAS_WIDTH, ATLAS_HEIGHT, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
99+
this._textureAllocated = true;
100+
}
101+
102+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this._canvas);
103+
104+
gl.bindTexture(gl.TEXTURE_2D, null);
105+
this._dirty = false;
106+
}
107+
108+
clear(): void {
109+
this._cache.clear();
110+
this._pending.clear();
111+
this._shelves = [];
112+
this._dirty = false;
113+
this._ctx.clearRect(0, 0, ATLAS_WIDTH, ATLAS_HEIGHT);
114+
}
115+
116+
private _packImage(url: string, image: HTMLImageElement): ImageAtlasEntry | null {
117+
if (!image.naturalWidth || !image.naturalHeight) {
118+
return null;
119+
}
120+
121+
const aspect = image.naturalWidth / image.naturalHeight;
122+
123+
let drawW: number;
124+
let drawH: number;
125+
if (image.naturalWidth >= image.naturalHeight) {
126+
drawW = Math.min(image.naturalWidth, MAX_CELL_SIZE);
127+
drawH = Math.round(drawW / aspect);
128+
} else {
129+
drawH = Math.min(image.naturalHeight, MAX_CELL_SIZE);
130+
drawW = Math.round(drawH * aspect);
131+
}
132+
133+
const cellW = drawW + PADDING * 2;
134+
const cellH = drawH + PADDING * 2;
135+
136+
const slot = this._allocate(cellW, cellH);
137+
if (!slot) {
138+
return null;
139+
}
140+
141+
this._ctx.drawImage(image, slot.x + PADDING, slot.y + PADDING, drawW, drawH);
142+
143+
const entry: ImageAtlasEntry = {
144+
u0: (slot.x + PADDING) / ATLAS_WIDTH,
145+
v0: (slot.y + PADDING) / ATLAS_HEIGHT,
146+
u1: (slot.x + PADDING + drawW) / ATLAS_WIDTH,
147+
v1: (slot.y + PADDING + drawH) / ATLAS_HEIGHT,
148+
aspect,
149+
};
150+
151+
this._cache.set(url, entry);
152+
this._pending.delete(url);
153+
this._dirty = true;
154+
return entry;
155+
}
156+
157+
private _allocate(w: number, h: number): { x: number; y: number } | null {
158+
for (let i = 0; i < this._shelves.length; i++) {
159+
const shelf = this._shelves[i];
160+
if (shelf.x + w <= ATLAS_WIDTH && h <= shelf.height) {
161+
const pos = { x: shelf.x, y: shelf.y };
162+
shelf.x += w;
163+
return pos;
164+
}
165+
}
166+
167+
const shelfY =
168+
this._shelves.length === 0
169+
? 0
170+
: this._shelves[this._shelves.length - 1].y + this._shelves[this._shelves.length - 1].height;
171+
172+
if (shelfY + h > ATLAS_HEIGHT) {
173+
return null;
174+
}
175+
176+
const newShelf: Shelf = { y: shelfY, height: h, x: w };
177+
this._shelves.push(newShelf);
178+
return { x: 0, y: shelfY };
179+
}
180+
}

0 commit comments

Comments
 (0)