@next_term/web 0.1.0-next.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/dist/accessibility.d.ts +46 -0
- package/dist/accessibility.d.ts.map +1 -0
- package/dist/accessibility.js +196 -0
- package/dist/accessibility.js.map +1 -0
- package/dist/addon.d.ts.map +1 -0
- package/dist/addon.js +2 -0
- package/dist/addon.js.map +1 -0
- package/dist/addons/fit.d.ts.map +1 -0
- package/dist/addons/fit.js +40 -0
- package/dist/addons/fit.js.map +1 -0
- package/dist/addons/search.d.ts +56 -0
- package/dist/addons/search.d.ts.map +1 -0
- package/dist/addons/search.js +178 -0
- package/dist/addons/search.js.map +1 -0
- package/dist/addons/web-links.d.ts +30 -0
- package/dist/addons/web-links.d.ts.map +1 -0
- package/dist/addons/web-links.js +170 -0
- package/dist/addons/web-links.js.map +1 -0
- package/dist/fit.d.ts.map +1 -0
- package/dist/fit.js +14 -0
- package/dist/fit.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/input-handler.d.ts +185 -0
- package/dist/input-handler.d.ts.map +1 -0
- package/dist/input-handler.js +1197 -0
- package/dist/input-handler.js.map +1 -0
- package/dist/parser-worker.d.ts.map +1 -0
- package/dist/parser-worker.js +128 -0
- package/dist/parser-worker.js.map +1 -0
- package/dist/render-bridge.d.ts +56 -0
- package/dist/render-bridge.d.ts.map +1 -0
- package/dist/render-bridge.js +158 -0
- package/dist/render-bridge.js.map +1 -0
- package/dist/render-worker.d.ts +62 -0
- package/dist/render-worker.d.ts.map +1 -0
- package/dist/render-worker.js +720 -0
- package/dist/render-worker.js.map +1 -0
- package/dist/renderer.d.ts +86 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +454 -0
- package/dist/renderer.js.map +1 -0
- package/dist/shared-context.d.ts +93 -0
- package/dist/shared-context.d.ts.map +1 -0
- package/dist/shared-context.js +561 -0
- package/dist/shared-context.js.map +1 -0
- package/dist/web-terminal.d.ts +152 -0
- package/dist/web-terminal.d.ts.map +1 -0
- package/dist/web-terminal.js +684 -0
- package/dist/web-terminal.js.map +1 -0
- package/dist/webgl-renderer.d.ts +146 -0
- package/dist/webgl-renderer.d.ts.map +1 -0
- package/dist/webgl-renderer.js +1047 -0
- package/dist/webgl-renderer.js.map +1 -0
- package/dist/worker-bridge.d.ts +51 -0
- package/dist/worker-bridge.d.ts.map +1 -0
- package/dist/worker-bridge.js +185 -0
- package/dist/worker-bridge.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,1047 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebGL2 renderer for react-term.
|
|
3
|
+
*
|
|
4
|
+
* Architecture (inspired by Alacritty, Warp, xterm.js WebGL addon):
|
|
5
|
+
* - Two draw calls per frame: backgrounds (instanced rects) + foreground (instanced glyphs)
|
|
6
|
+
* - Alpha-only glyph texture atlas with color multiplication at render time
|
|
7
|
+
* - Instance-based rendering via drawElementsInstanced
|
|
8
|
+
*/
|
|
9
|
+
import { DEFAULT_THEME, normalizeSelection } from "@next_term/core";
|
|
10
|
+
import { build256Palette, Canvas2DRenderer } from "./renderer.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Attribute bit positions (mirrors renderer.ts / cell-grid.ts)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const ATTR_BOLD = 0x01;
|
|
15
|
+
const ATTR_ITALIC = 0x02;
|
|
16
|
+
const _ATTR_UNDERLINE = 0x04;
|
|
17
|
+
const _ATTR_STRIKETHROUGH = 0x08;
|
|
18
|
+
const ATTR_INVERSE = 0x40;
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Color helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Parse a hex color (#rrggbb or #rgb) to [r, g, b, a] in 0-1 range. */
|
|
23
|
+
export function hexToFloat4(hex) {
|
|
24
|
+
let r = 0, g = 0, b = 0;
|
|
25
|
+
if (hex.startsWith("#")) {
|
|
26
|
+
const h = hex.slice(1);
|
|
27
|
+
if (h.length === 3) {
|
|
28
|
+
r = parseInt(h[0] + h[0], 16) / 255;
|
|
29
|
+
g = parseInt(h[1] + h[1], 16) / 255;
|
|
30
|
+
b = parseInt(h[2] + h[2], 16) / 255;
|
|
31
|
+
}
|
|
32
|
+
else if (h.length === 6) {
|
|
33
|
+
r = parseInt(h.slice(0, 2), 16) / 255;
|
|
34
|
+
g = parseInt(h.slice(2, 4), 16) / 255;
|
|
35
|
+
b = parseInt(h.slice(4, 6), 16) / 255;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (hex.startsWith("rgb(")) {
|
|
39
|
+
const m = hex.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
40
|
+
if (m) {
|
|
41
|
+
r = parseInt(m[1], 10) / 255;
|
|
42
|
+
g = parseInt(m[2], 10) / 255;
|
|
43
|
+
b = parseInt(m[3], 10) / 255;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [r, g, b, 1.0];
|
|
47
|
+
}
|
|
48
|
+
/** Build a glyph cache key from codepoint and style flags. */
|
|
49
|
+
export function glyphCacheKey(codepoint, bold, italic) {
|
|
50
|
+
return `${codepoint}_${bold ? 1 : 0}_${italic ? 1 : 0}`;
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// GlyphAtlas
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export class GlyphAtlas {
|
|
56
|
+
texture = null;
|
|
57
|
+
canvas = null;
|
|
58
|
+
ctx = null;
|
|
59
|
+
cache = new Map();
|
|
60
|
+
nextX = 0;
|
|
61
|
+
nextY = 0;
|
|
62
|
+
rowHeight = 0;
|
|
63
|
+
width;
|
|
64
|
+
height;
|
|
65
|
+
dirty = false;
|
|
66
|
+
fontSize;
|
|
67
|
+
fontFamily;
|
|
68
|
+
constructor(fontSize, fontFamily, initialSize = 512) {
|
|
69
|
+
this.fontSize = fontSize;
|
|
70
|
+
this.fontFamily = fontFamily;
|
|
71
|
+
this.width = initialSize;
|
|
72
|
+
this.height = initialSize;
|
|
73
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
74
|
+
this.canvas = new OffscreenCanvas(this.width, this.height);
|
|
75
|
+
const ctx = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
76
|
+
if (!ctx)
|
|
77
|
+
throw new Error("Failed to get 2d context for glyph atlas");
|
|
78
|
+
this.ctx = ctx;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get (or rasterize) a glyph. Returns the GlyphInfo with atlas coordinates.
|
|
83
|
+
*/
|
|
84
|
+
getGlyph(codepoint, bold, italic) {
|
|
85
|
+
const key = glyphCacheKey(codepoint, bold, italic);
|
|
86
|
+
const cached = this.cache.get(key);
|
|
87
|
+
if (cached)
|
|
88
|
+
return cached;
|
|
89
|
+
if (!this.ctx || !this.canvas)
|
|
90
|
+
return null;
|
|
91
|
+
const ch = String.fromCodePoint(codepoint);
|
|
92
|
+
const font = this.buildFont(bold, italic);
|
|
93
|
+
this.ctx.font = font;
|
|
94
|
+
const metrics = this.ctx.measureText(ch);
|
|
95
|
+
const pw = Math.ceil(metrics.width) + 2; // 1px padding each side
|
|
96
|
+
const ascent = typeof metrics.fontBoundingBoxAscent === "number"
|
|
97
|
+
? metrics.fontBoundingBoxAscent
|
|
98
|
+
: this.fontSize;
|
|
99
|
+
const descent = typeof metrics.fontBoundingBoxDescent === "number"
|
|
100
|
+
? metrics.fontBoundingBoxDescent
|
|
101
|
+
: Math.ceil(this.fontSize * 0.2);
|
|
102
|
+
const ph = Math.ceil(ascent + descent) + 2;
|
|
103
|
+
// Check if we need to wrap to next row
|
|
104
|
+
if (this.nextX + pw > this.width) {
|
|
105
|
+
this.nextX = 0;
|
|
106
|
+
this.nextY += this.rowHeight;
|
|
107
|
+
this.rowHeight = 0;
|
|
108
|
+
}
|
|
109
|
+
// Check if we need to grow the atlas
|
|
110
|
+
if (this.nextY + ph > this.height) {
|
|
111
|
+
const newHeight = Math.min(this.height * 2, 4096);
|
|
112
|
+
if (newHeight <= this.height) {
|
|
113
|
+
// Can't grow further; return null as we've hit the limit
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
this.growAtlas(this.width, newHeight);
|
|
117
|
+
}
|
|
118
|
+
// Rasterize
|
|
119
|
+
this.ctx.font = font;
|
|
120
|
+
this.ctx.fillStyle = "white";
|
|
121
|
+
this.ctx.textBaseline = "alphabetic";
|
|
122
|
+
this.ctx.fillText(ch, this.nextX + 1, this.nextY + 1 + ascent);
|
|
123
|
+
const info = {
|
|
124
|
+
u: this.nextX / this.width,
|
|
125
|
+
v: this.nextY / this.height,
|
|
126
|
+
w: pw / this.width,
|
|
127
|
+
h: ph / this.height,
|
|
128
|
+
pw,
|
|
129
|
+
ph,
|
|
130
|
+
};
|
|
131
|
+
this.cache.set(key, info);
|
|
132
|
+
this.nextX += pw;
|
|
133
|
+
this.rowHeight = Math.max(this.rowHeight, ph);
|
|
134
|
+
this.dirty = true;
|
|
135
|
+
return info;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Upload the atlas texture to GPU. Call once per frame if dirty.
|
|
139
|
+
*/
|
|
140
|
+
upload(gl) {
|
|
141
|
+
if (!this.canvas)
|
|
142
|
+
return;
|
|
143
|
+
if (!this.texture) {
|
|
144
|
+
this.texture = gl.createTexture();
|
|
145
|
+
}
|
|
146
|
+
if (!this.dirty && this.texture)
|
|
147
|
+
return;
|
|
148
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
149
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
150
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas);
|
|
151
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
152
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
153
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
154
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
155
|
+
this.dirty = false;
|
|
156
|
+
}
|
|
157
|
+
getTexture() {
|
|
158
|
+
return this.texture;
|
|
159
|
+
}
|
|
160
|
+
/** Recreate GL texture (for context restore). */
|
|
161
|
+
recreateTexture() {
|
|
162
|
+
this.texture = null;
|
|
163
|
+
this.dirty = true;
|
|
164
|
+
}
|
|
165
|
+
dispose(gl) {
|
|
166
|
+
if (gl && this.texture) {
|
|
167
|
+
gl.deleteTexture(this.texture);
|
|
168
|
+
}
|
|
169
|
+
this.texture = null;
|
|
170
|
+
this.cache.clear();
|
|
171
|
+
}
|
|
172
|
+
buildFont(bold, italic) {
|
|
173
|
+
let font = "";
|
|
174
|
+
if (italic)
|
|
175
|
+
font += "italic ";
|
|
176
|
+
if (bold)
|
|
177
|
+
font += "bold ";
|
|
178
|
+
font += `${this.fontSize}px ${this.fontFamily}`;
|
|
179
|
+
return font;
|
|
180
|
+
}
|
|
181
|
+
growAtlas(newWidth, newHeight) {
|
|
182
|
+
if (!this.canvas || !this.ctx)
|
|
183
|
+
return;
|
|
184
|
+
// Save existing content
|
|
185
|
+
const imageData = this.ctx.getImageData(0, 0, this.width, this.height);
|
|
186
|
+
this.width = newWidth;
|
|
187
|
+
this.height = newHeight;
|
|
188
|
+
this.canvas.width = newWidth;
|
|
189
|
+
this.canvas.height = newHeight;
|
|
190
|
+
// Restore content
|
|
191
|
+
this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
192
|
+
if (!this.ctx)
|
|
193
|
+
return;
|
|
194
|
+
this.ctx.putImageData(imageData, 0, 0);
|
|
195
|
+
// Recalculate UV coordinates for all cached glyphs
|
|
196
|
+
for (const [_key, info] of this.cache) {
|
|
197
|
+
// Convert back to pixel coords and recalculate
|
|
198
|
+
const px = info.u * imageData.width;
|
|
199
|
+
const py = info.v * imageData.height;
|
|
200
|
+
info.u = px / newWidth;
|
|
201
|
+
info.v = py / newHeight;
|
|
202
|
+
info.w = info.pw / newWidth;
|
|
203
|
+
info.h = info.ph / newHeight;
|
|
204
|
+
}
|
|
205
|
+
this.dirty = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Shader sources
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
const BG_VERTEX_SHADER = `#version 300 es
|
|
212
|
+
// Per-vertex: unit quad
|
|
213
|
+
in vec2 a_position;
|
|
214
|
+
|
|
215
|
+
// Per-instance
|
|
216
|
+
in vec2 a_cellPos;
|
|
217
|
+
in vec4 a_color;
|
|
218
|
+
|
|
219
|
+
uniform vec2 u_resolution;
|
|
220
|
+
uniform vec2 u_cellSize;
|
|
221
|
+
|
|
222
|
+
out vec4 v_color;
|
|
223
|
+
|
|
224
|
+
void main() {
|
|
225
|
+
vec2 cellPixelPos = a_cellPos * u_cellSize;
|
|
226
|
+
vec2 pos = cellPixelPos + a_position * u_cellSize;
|
|
227
|
+
|
|
228
|
+
vec2 clipPos = (pos / u_resolution) * 2.0 - 1.0;
|
|
229
|
+
clipPos.y = -clipPos.y;
|
|
230
|
+
gl_Position = vec4(clipPos, 0.0, 1.0);
|
|
231
|
+
|
|
232
|
+
v_color = a_color;
|
|
233
|
+
}
|
|
234
|
+
`;
|
|
235
|
+
const BG_FRAGMENT_SHADER = `#version 300 es
|
|
236
|
+
precision mediump float;
|
|
237
|
+
in vec4 v_color;
|
|
238
|
+
out vec4 fragColor;
|
|
239
|
+
void main() {
|
|
240
|
+
fragColor = v_color;
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
const GLYPH_VERTEX_SHADER = `#version 300 es
|
|
244
|
+
in vec2 a_position;
|
|
245
|
+
|
|
246
|
+
// Per-instance
|
|
247
|
+
in vec2 a_cellPos;
|
|
248
|
+
in vec4 a_color;
|
|
249
|
+
in vec4 a_texCoord;
|
|
250
|
+
in vec2 a_glyphSize;
|
|
251
|
+
|
|
252
|
+
uniform vec2 u_resolution;
|
|
253
|
+
uniform vec2 u_cellSize;
|
|
254
|
+
|
|
255
|
+
out vec4 v_color;
|
|
256
|
+
out vec2 v_texCoord;
|
|
257
|
+
|
|
258
|
+
void main() {
|
|
259
|
+
vec2 cellPixelPos = a_cellPos * u_cellSize;
|
|
260
|
+
vec2 size = (a_glyphSize.x > 0.0) ? a_glyphSize : u_cellSize;
|
|
261
|
+
vec2 pos = cellPixelPos + a_position * size;
|
|
262
|
+
|
|
263
|
+
vec2 clipPos = (pos / u_resolution) * 2.0 - 1.0;
|
|
264
|
+
clipPos.y = -clipPos.y;
|
|
265
|
+
gl_Position = vec4(clipPos, 0.0, 1.0);
|
|
266
|
+
|
|
267
|
+
v_color = a_color;
|
|
268
|
+
v_texCoord = a_texCoord.xy + a_position * a_texCoord.zw;
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
const GLYPH_FRAGMENT_SHADER = `#version 300 es
|
|
272
|
+
precision mediump float;
|
|
273
|
+
in vec4 v_color;
|
|
274
|
+
in vec2 v_texCoord;
|
|
275
|
+
uniform sampler2D u_atlas;
|
|
276
|
+
out vec4 fragColor;
|
|
277
|
+
void main() {
|
|
278
|
+
float alpha = texture(u_atlas, v_texCoord).a;
|
|
279
|
+
fragColor = vec4(v_color.rgb, v_color.a * alpha);
|
|
280
|
+
}
|
|
281
|
+
`;
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Instance buffer packing helpers
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
/** Floats per background instance: cellCol, cellRow, r, g, b, a */
|
|
286
|
+
export const BG_INSTANCE_FLOATS = 6;
|
|
287
|
+
/** Floats per glyph instance: cellCol, cellRow, r, g, b, a, u, v, tw, th, pw, ph */
|
|
288
|
+
export const GLYPH_INSTANCE_FLOATS = 12;
|
|
289
|
+
/**
|
|
290
|
+
* Pack a background instance into a Float32Array at the given offset.
|
|
291
|
+
*/
|
|
292
|
+
export function packBgInstance(buf, offset, col, row, r, g, b, a) {
|
|
293
|
+
buf[offset] = col;
|
|
294
|
+
buf[offset + 1] = row;
|
|
295
|
+
buf[offset + 2] = r;
|
|
296
|
+
buf[offset + 3] = g;
|
|
297
|
+
buf[offset + 4] = b;
|
|
298
|
+
buf[offset + 5] = a;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Pack a glyph instance into a Float32Array at the given offset.
|
|
302
|
+
*/
|
|
303
|
+
export function packGlyphInstance(buf, offset, col, row, r, g, b, a, u, v, tw, th, pw, ph) {
|
|
304
|
+
buf[offset] = col;
|
|
305
|
+
buf[offset + 1] = row;
|
|
306
|
+
buf[offset + 2] = r;
|
|
307
|
+
buf[offset + 3] = g;
|
|
308
|
+
buf[offset + 4] = b;
|
|
309
|
+
buf[offset + 5] = a;
|
|
310
|
+
buf[offset + 6] = u;
|
|
311
|
+
buf[offset + 7] = v;
|
|
312
|
+
buf[offset + 8] = tw;
|
|
313
|
+
buf[offset + 9] = th;
|
|
314
|
+
buf[offset + 10] = pw;
|
|
315
|
+
buf[offset + 11] = ph;
|
|
316
|
+
}
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Shader compilation helpers
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
function compileShader(gl, type, source) {
|
|
321
|
+
const shader = gl.createShader(type);
|
|
322
|
+
if (!shader)
|
|
323
|
+
throw new Error("Failed to create shader");
|
|
324
|
+
gl.shaderSource(shader, source);
|
|
325
|
+
gl.compileShader(shader);
|
|
326
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
327
|
+
const info = gl.getShaderInfoLog(shader);
|
|
328
|
+
gl.deleteShader(shader);
|
|
329
|
+
throw new Error(`Shader compile error: ${info}`);
|
|
330
|
+
}
|
|
331
|
+
return shader;
|
|
332
|
+
}
|
|
333
|
+
function createProgram(gl, vertSrc, fragSrc) {
|
|
334
|
+
const vert = compileShader(gl, gl.VERTEX_SHADER, vertSrc);
|
|
335
|
+
const frag = compileShader(gl, gl.FRAGMENT_SHADER, fragSrc);
|
|
336
|
+
const program = gl.createProgram();
|
|
337
|
+
if (!program)
|
|
338
|
+
throw new Error("Failed to create program");
|
|
339
|
+
gl.attachShader(program, vert);
|
|
340
|
+
gl.attachShader(program, frag);
|
|
341
|
+
gl.linkProgram(program);
|
|
342
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
343
|
+
const info = gl.getProgramInfoLog(program);
|
|
344
|
+
gl.deleteProgram(program);
|
|
345
|
+
throw new Error(`Program link error: ${info}`);
|
|
346
|
+
}
|
|
347
|
+
// Shaders can be detached after linking
|
|
348
|
+
gl.detachShader(program, vert);
|
|
349
|
+
gl.detachShader(program, frag);
|
|
350
|
+
gl.deleteShader(vert);
|
|
351
|
+
gl.deleteShader(frag);
|
|
352
|
+
return program;
|
|
353
|
+
}
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// WebGLRenderer
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
export class WebGLRenderer {
|
|
358
|
+
canvas = null;
|
|
359
|
+
gl = null;
|
|
360
|
+
grid = null;
|
|
361
|
+
cursor = null;
|
|
362
|
+
cellWidth = 0;
|
|
363
|
+
cellHeight = 0;
|
|
364
|
+
baselineOffset = 0;
|
|
365
|
+
fontSize;
|
|
366
|
+
fontFamily;
|
|
367
|
+
theme;
|
|
368
|
+
dpr;
|
|
369
|
+
palette;
|
|
370
|
+
selection = null;
|
|
371
|
+
highlights = [];
|
|
372
|
+
// Track previous cursor position to force redraw when cursor moves
|
|
373
|
+
prevCursorRow = -1;
|
|
374
|
+
prevCursorCol = -1;
|
|
375
|
+
rafId = null;
|
|
376
|
+
disposed = false;
|
|
377
|
+
contextLost = false;
|
|
378
|
+
// GL resources
|
|
379
|
+
bgProgram = null;
|
|
380
|
+
glyphProgram = null;
|
|
381
|
+
quadVBO = null;
|
|
382
|
+
quadEBO = null;
|
|
383
|
+
bgInstanceVBO = null;
|
|
384
|
+
glyphInstanceVBO = null;
|
|
385
|
+
bgVAO = null;
|
|
386
|
+
glyphVAO = null;
|
|
387
|
+
// Instance data (CPU side)
|
|
388
|
+
bgInstances;
|
|
389
|
+
glyphInstances;
|
|
390
|
+
bgCount = 0;
|
|
391
|
+
glyphCount = 0;
|
|
392
|
+
// Glyph atlas
|
|
393
|
+
atlas;
|
|
394
|
+
// Palette as float arrays (cached for performance)
|
|
395
|
+
paletteFloat = [];
|
|
396
|
+
themeFgFloat = [0, 0, 0, 1];
|
|
397
|
+
themeBgFloat = [0, 0, 0, 1];
|
|
398
|
+
themeCursorFloat = [0, 0, 0, 1];
|
|
399
|
+
// Context loss handlers
|
|
400
|
+
handleContextLost = null;
|
|
401
|
+
handleContextRestored = null;
|
|
402
|
+
constructor(options) {
|
|
403
|
+
this.fontSize = options.fontSize;
|
|
404
|
+
this.fontFamily = options.fontFamily;
|
|
405
|
+
this.theme = options.theme ?? DEFAULT_THEME;
|
|
406
|
+
this.dpr =
|
|
407
|
+
options.devicePixelRatio ?? (typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1);
|
|
408
|
+
this.palette = build256Palette(this.theme);
|
|
409
|
+
this.measureCellSize();
|
|
410
|
+
this.buildPaletteFloat();
|
|
411
|
+
this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily);
|
|
412
|
+
// Pre-allocate instance buffers for a reasonable default size
|
|
413
|
+
const maxCells = 80 * 24;
|
|
414
|
+
this.bgInstances = new Float32Array(maxCells * BG_INSTANCE_FLOATS);
|
|
415
|
+
this.glyphInstances = new Float32Array(maxCells * GLYPH_INSTANCE_FLOATS);
|
|
416
|
+
}
|
|
417
|
+
// -----------------------------------------------------------------------
|
|
418
|
+
// IRenderer
|
|
419
|
+
// -----------------------------------------------------------------------
|
|
420
|
+
attach(canvas, grid, cursor) {
|
|
421
|
+
this.canvas = canvas;
|
|
422
|
+
this.grid = grid;
|
|
423
|
+
this.cursor = cursor;
|
|
424
|
+
// Get WebGL2 context
|
|
425
|
+
this.gl = canvas.getContext("webgl2", {
|
|
426
|
+
alpha: false,
|
|
427
|
+
antialias: false,
|
|
428
|
+
premultipliedAlpha: false,
|
|
429
|
+
preserveDrawingBuffer: false,
|
|
430
|
+
});
|
|
431
|
+
if (!this.gl) {
|
|
432
|
+
throw new Error("WebGL2 is not available");
|
|
433
|
+
}
|
|
434
|
+
// Set up context loss handlers
|
|
435
|
+
this.handleContextLost = (e) => {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
this.contextLost = true;
|
|
438
|
+
this.stopRenderLoop();
|
|
439
|
+
};
|
|
440
|
+
this.handleContextRestored = () => {
|
|
441
|
+
this.contextLost = false;
|
|
442
|
+
this.initGLResources();
|
|
443
|
+
if (this.grid)
|
|
444
|
+
this.grid.markAllDirty();
|
|
445
|
+
this.startRenderLoop();
|
|
446
|
+
};
|
|
447
|
+
canvas.addEventListener("webglcontextlost", this.handleContextLost);
|
|
448
|
+
canvas.addEventListener("webglcontextrestored", this.handleContextRestored);
|
|
449
|
+
this.syncCanvasSize();
|
|
450
|
+
this.initGLResources();
|
|
451
|
+
this.ensureInstanceBuffers();
|
|
452
|
+
grid.markAllDirty();
|
|
453
|
+
}
|
|
454
|
+
render() {
|
|
455
|
+
if (this.disposed || this.contextLost || !this.gl || !this.grid || !this.cursor)
|
|
456
|
+
return;
|
|
457
|
+
const gl = this.gl;
|
|
458
|
+
const grid = this.grid;
|
|
459
|
+
const cols = grid.cols;
|
|
460
|
+
const rows = grid.rows;
|
|
461
|
+
// If cursor moved, mark old and new rows dirty to erase ghost and draw fresh
|
|
462
|
+
const curRow = this.cursor.row;
|
|
463
|
+
const curCol = this.cursor.col;
|
|
464
|
+
if (this.prevCursorRow >= 0 &&
|
|
465
|
+
this.prevCursorRow < rows &&
|
|
466
|
+
(this.prevCursorRow !== curRow || this.prevCursorCol !== curCol)) {
|
|
467
|
+
grid.markDirty(this.prevCursorRow);
|
|
468
|
+
}
|
|
469
|
+
if (curRow >= 0 && curRow < rows) {
|
|
470
|
+
grid.markDirty(curRow);
|
|
471
|
+
}
|
|
472
|
+
this.prevCursorRow = curRow;
|
|
473
|
+
this.prevCursorCol = curCol;
|
|
474
|
+
// Check if any rows are dirty
|
|
475
|
+
let anyDirty = false;
|
|
476
|
+
for (let r = 0; r < rows; r++) {
|
|
477
|
+
if (grid.isDirty(r)) {
|
|
478
|
+
anyDirty = true;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!anyDirty) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// Rebuild instance data
|
|
486
|
+
this.bgCount = 0;
|
|
487
|
+
this.glyphCount = 0;
|
|
488
|
+
for (let row = 0; row < rows; row++) {
|
|
489
|
+
for (let col = 0; col < cols; col++) {
|
|
490
|
+
const codepoint = grid.getCodepoint(row, col);
|
|
491
|
+
const fgIdx = grid.getFgIndex(row, col);
|
|
492
|
+
const bgIdx = grid.getBgIndex(row, col);
|
|
493
|
+
const attrs = grid.getAttrs(row, col);
|
|
494
|
+
const fgIsRGB = grid.isFgRGB(row, col);
|
|
495
|
+
const bgIsRGB = grid.isBgRGB(row, col);
|
|
496
|
+
const isWide = grid.isWide(row, col);
|
|
497
|
+
let fg = this.resolveColorFloat(fgIdx, fgIsRGB, grid, col, true);
|
|
498
|
+
let bg = this.resolveColorFloat(bgIdx, bgIsRGB, grid, col, false);
|
|
499
|
+
// Handle inverse
|
|
500
|
+
if (attrs & ATTR_INVERSE) {
|
|
501
|
+
const tmp = fg;
|
|
502
|
+
fg = bg;
|
|
503
|
+
bg = tmp;
|
|
504
|
+
}
|
|
505
|
+
// Background instance — emit for all cells to paint default bg too
|
|
506
|
+
packBgInstance(this.bgInstances, this.bgCount * BG_INSTANCE_FLOATS, col, row, bg[0], bg[1], bg[2], bg[3]);
|
|
507
|
+
this.bgCount++;
|
|
508
|
+
// Glyph instance — skip spaces and control chars
|
|
509
|
+
if (codepoint > 0x20) {
|
|
510
|
+
const bold = !!(attrs & ATTR_BOLD);
|
|
511
|
+
const italic = !!(attrs & ATTR_ITALIC);
|
|
512
|
+
const glyph = this.atlas.getGlyph(codepoint, bold, italic);
|
|
513
|
+
if (glyph) {
|
|
514
|
+
const glyphPw = isWide ? glyph.pw : glyph.pw;
|
|
515
|
+
const glyphPh = glyph.ph;
|
|
516
|
+
packGlyphInstance(this.glyphInstances, this.glyphCount * GLYPH_INSTANCE_FLOATS, col, row, fg[0], fg[1], fg[2], fg[3], glyph.u, glyph.v, glyph.w, glyph.h, glyphPw, glyphPh);
|
|
517
|
+
this.glyphCount++;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
grid.clearDirty(row);
|
|
522
|
+
}
|
|
523
|
+
// Upload atlas if dirty
|
|
524
|
+
this.atlas.upload(gl);
|
|
525
|
+
// Set up GL state
|
|
526
|
+
const canvasWidth = this.canvas?.width ?? 0;
|
|
527
|
+
const canvasHeight = this.canvas?.height ?? 0;
|
|
528
|
+
gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
529
|
+
gl.clearColor(this.themeBgFloat[0], this.themeBgFloat[1], this.themeBgFloat[2], 1.0);
|
|
530
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
531
|
+
const cellW = this.cellWidth * this.dpr;
|
|
532
|
+
const cellH = this.cellHeight * this.dpr;
|
|
533
|
+
// --- Background pass ---
|
|
534
|
+
if (this.bgCount > 0 && this.bgProgram && this.bgVAO && this.bgInstanceVBO) {
|
|
535
|
+
gl.useProgram(this.bgProgram);
|
|
536
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), canvasWidth, canvasHeight);
|
|
537
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), cellW, cellH);
|
|
538
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
|
|
539
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, this.bgCount * BG_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
|
|
540
|
+
gl.bindVertexArray(this.bgVAO);
|
|
541
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.bgCount);
|
|
542
|
+
}
|
|
543
|
+
// --- Glyph pass ---
|
|
544
|
+
if (this.glyphCount > 0 && this.glyphProgram && this.glyphVAO && this.glyphInstanceVBO) {
|
|
545
|
+
gl.enable(gl.BLEND);
|
|
546
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
547
|
+
gl.useProgram(this.glyphProgram);
|
|
548
|
+
gl.uniform2f(gl.getUniformLocation(this.glyphProgram, "u_resolution"), canvasWidth, canvasHeight);
|
|
549
|
+
gl.uniform2f(gl.getUniformLocation(this.glyphProgram, "u_cellSize"), cellW, cellH);
|
|
550
|
+
// Bind atlas texture
|
|
551
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
552
|
+
gl.bindTexture(gl.TEXTURE_2D, this.atlas.getTexture());
|
|
553
|
+
gl.uniform1i(gl.getUniformLocation(this.glyphProgram, "u_atlas"), 0);
|
|
554
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBO);
|
|
555
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, this.glyphCount * GLYPH_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
|
|
556
|
+
gl.bindVertexArray(this.glyphVAO);
|
|
557
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.glyphCount);
|
|
558
|
+
gl.disable(gl.BLEND);
|
|
559
|
+
}
|
|
560
|
+
// --- Highlights (search results) ---
|
|
561
|
+
this.drawHighlights();
|
|
562
|
+
// --- Selection overlay ---
|
|
563
|
+
this.drawSelection();
|
|
564
|
+
// --- Cursor ---
|
|
565
|
+
this.drawCursor();
|
|
566
|
+
gl.bindVertexArray(null);
|
|
567
|
+
}
|
|
568
|
+
resize(_cols, _rows) {
|
|
569
|
+
if (!this.canvas || !this.grid)
|
|
570
|
+
return;
|
|
571
|
+
this.syncCanvasSize();
|
|
572
|
+
this.ensureInstanceBuffers();
|
|
573
|
+
this.grid.markAllDirty();
|
|
574
|
+
}
|
|
575
|
+
setTheme(theme) {
|
|
576
|
+
this.theme = theme;
|
|
577
|
+
this.palette = build256Palette(theme);
|
|
578
|
+
this.buildPaletteFloat();
|
|
579
|
+
if (this.grid) {
|
|
580
|
+
this.grid.markAllDirty();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
setSelection(selection) {
|
|
584
|
+
this.selection = selection;
|
|
585
|
+
if (this.grid) {
|
|
586
|
+
this.grid.markAllDirty();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
setHighlights(highlights) {
|
|
590
|
+
this.highlights = highlights;
|
|
591
|
+
if (this.grid) {
|
|
592
|
+
this.grid.markAllDirty();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
setFont(fontSize, fontFamily) {
|
|
596
|
+
this.fontSize = fontSize;
|
|
597
|
+
this.fontFamily = fontFamily;
|
|
598
|
+
this.measureCellSize();
|
|
599
|
+
// Recreate atlas with new font size
|
|
600
|
+
if (this.gl) {
|
|
601
|
+
this.atlas.dispose(this.gl);
|
|
602
|
+
}
|
|
603
|
+
this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily);
|
|
604
|
+
if (this.grid) {
|
|
605
|
+
this.syncCanvasSize();
|
|
606
|
+
this.grid.markAllDirty();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
getCellSize() {
|
|
610
|
+
return { width: this.cellWidth, height: this.cellHeight };
|
|
611
|
+
}
|
|
612
|
+
dispose() {
|
|
613
|
+
this.disposed = true;
|
|
614
|
+
this.stopRenderLoop();
|
|
615
|
+
if (this.canvas) {
|
|
616
|
+
if (this.handleContextLost) {
|
|
617
|
+
this.canvas.removeEventListener("webglcontextlost", this.handleContextLost);
|
|
618
|
+
}
|
|
619
|
+
if (this.handleContextRestored) {
|
|
620
|
+
this.canvas.removeEventListener("webglcontextrestored", this.handleContextRestored);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const gl = this.gl;
|
|
624
|
+
if (gl) {
|
|
625
|
+
this.atlas.dispose(gl);
|
|
626
|
+
if (this.bgProgram)
|
|
627
|
+
gl.deleteProgram(this.bgProgram);
|
|
628
|
+
if (this.glyphProgram)
|
|
629
|
+
gl.deleteProgram(this.glyphProgram);
|
|
630
|
+
if (this.quadVBO)
|
|
631
|
+
gl.deleteBuffer(this.quadVBO);
|
|
632
|
+
if (this.quadEBO)
|
|
633
|
+
gl.deleteBuffer(this.quadEBO);
|
|
634
|
+
if (this.bgInstanceVBO)
|
|
635
|
+
gl.deleteBuffer(this.bgInstanceVBO);
|
|
636
|
+
if (this.glyphInstanceVBO)
|
|
637
|
+
gl.deleteBuffer(this.glyphInstanceVBO);
|
|
638
|
+
if (this.bgVAO)
|
|
639
|
+
gl.deleteVertexArray(this.bgVAO);
|
|
640
|
+
if (this.glyphVAO)
|
|
641
|
+
gl.deleteVertexArray(this.glyphVAO);
|
|
642
|
+
}
|
|
643
|
+
this.canvas = null;
|
|
644
|
+
this.gl = null;
|
|
645
|
+
this.grid = null;
|
|
646
|
+
this.cursor = null;
|
|
647
|
+
}
|
|
648
|
+
// -----------------------------------------------------------------------
|
|
649
|
+
// Render loop
|
|
650
|
+
// -----------------------------------------------------------------------
|
|
651
|
+
startRenderLoop() {
|
|
652
|
+
if (this.disposed)
|
|
653
|
+
return;
|
|
654
|
+
const loop = () => {
|
|
655
|
+
if (this.disposed)
|
|
656
|
+
return;
|
|
657
|
+
this.render();
|
|
658
|
+
this.rafId = requestAnimationFrame(loop);
|
|
659
|
+
};
|
|
660
|
+
this.rafId = requestAnimationFrame(loop);
|
|
661
|
+
}
|
|
662
|
+
stopRenderLoop() {
|
|
663
|
+
if (this.rafId !== null) {
|
|
664
|
+
cancelAnimationFrame(this.rafId);
|
|
665
|
+
this.rafId = null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// -----------------------------------------------------------------------
|
|
669
|
+
// GL resource initialization
|
|
670
|
+
// -----------------------------------------------------------------------
|
|
671
|
+
initGLResources() {
|
|
672
|
+
const gl = this.gl;
|
|
673
|
+
if (!gl)
|
|
674
|
+
return;
|
|
675
|
+
// Compile programs
|
|
676
|
+
this.bgProgram = createProgram(gl, BG_VERTEX_SHADER, BG_FRAGMENT_SHADER);
|
|
677
|
+
this.glyphProgram = createProgram(gl, GLYPH_VERTEX_SHADER, GLYPH_FRAGMENT_SHADER);
|
|
678
|
+
// Unit quad vertices and indices
|
|
679
|
+
const quadVerts = new Float32Array([
|
|
680
|
+
0,
|
|
681
|
+
0, // bottom-left
|
|
682
|
+
1,
|
|
683
|
+
0, // bottom-right
|
|
684
|
+
0,
|
|
685
|
+
1, // top-left
|
|
686
|
+
1,
|
|
687
|
+
1, // top-right
|
|
688
|
+
]);
|
|
689
|
+
const quadIndices = new Uint16Array([0, 1, 2, 2, 1, 3]);
|
|
690
|
+
this.quadVBO = gl.createBuffer();
|
|
691
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVBO);
|
|
692
|
+
gl.bufferData(gl.ARRAY_BUFFER, quadVerts, gl.STATIC_DRAW);
|
|
693
|
+
this.quadEBO = gl.createBuffer();
|
|
694
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
695
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices, gl.STATIC_DRAW);
|
|
696
|
+
// Instance buffers
|
|
697
|
+
this.bgInstanceVBO = gl.createBuffer();
|
|
698
|
+
this.glyphInstanceVBO = gl.createBuffer();
|
|
699
|
+
// Set up background VAO
|
|
700
|
+
this.bgVAO = gl.createVertexArray();
|
|
701
|
+
gl.bindVertexArray(this.bgVAO);
|
|
702
|
+
this.setupBgVAO(gl);
|
|
703
|
+
gl.bindVertexArray(null);
|
|
704
|
+
// Set up glyph VAO
|
|
705
|
+
this.glyphVAO = gl.createVertexArray();
|
|
706
|
+
gl.bindVertexArray(this.glyphVAO);
|
|
707
|
+
this.setupGlyphVAO(gl);
|
|
708
|
+
gl.bindVertexArray(null);
|
|
709
|
+
// Recreate atlas texture
|
|
710
|
+
this.atlas.recreateTexture();
|
|
711
|
+
}
|
|
712
|
+
setupBgVAO(gl) {
|
|
713
|
+
const FLOAT = 4;
|
|
714
|
+
if (!this.bgProgram)
|
|
715
|
+
return;
|
|
716
|
+
const program = this.bgProgram;
|
|
717
|
+
// Quad vertex positions
|
|
718
|
+
const aPos = gl.getAttribLocation(program, "a_position");
|
|
719
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVBO);
|
|
720
|
+
gl.enableVertexAttribArray(aPos);
|
|
721
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
|
722
|
+
// Element buffer
|
|
723
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
724
|
+
// Instance data
|
|
725
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
|
|
726
|
+
const stride = BG_INSTANCE_FLOATS * FLOAT;
|
|
727
|
+
const aCellPos = gl.getAttribLocation(program, "a_cellPos");
|
|
728
|
+
gl.enableVertexAttribArray(aCellPos);
|
|
729
|
+
gl.vertexAttribPointer(aCellPos, 2, gl.FLOAT, false, stride, 0);
|
|
730
|
+
gl.vertexAttribDivisor(aCellPos, 1);
|
|
731
|
+
const aColor = gl.getAttribLocation(program, "a_color");
|
|
732
|
+
gl.enableVertexAttribArray(aColor);
|
|
733
|
+
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
734
|
+
gl.vertexAttribDivisor(aColor, 1);
|
|
735
|
+
}
|
|
736
|
+
setupGlyphVAO(gl) {
|
|
737
|
+
const FLOAT = 4;
|
|
738
|
+
if (!this.glyphProgram)
|
|
739
|
+
return;
|
|
740
|
+
const program = this.glyphProgram;
|
|
741
|
+
// Quad vertex positions
|
|
742
|
+
const aPos = gl.getAttribLocation(program, "a_position");
|
|
743
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVBO);
|
|
744
|
+
gl.enableVertexAttribArray(aPos);
|
|
745
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
|
746
|
+
// Element buffer
|
|
747
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
|
|
748
|
+
// Instance data
|
|
749
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBO);
|
|
750
|
+
const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
|
|
751
|
+
const aCellPos = gl.getAttribLocation(program, "a_cellPos");
|
|
752
|
+
gl.enableVertexAttribArray(aCellPos);
|
|
753
|
+
gl.vertexAttribPointer(aCellPos, 2, gl.FLOAT, false, stride, 0);
|
|
754
|
+
gl.vertexAttribDivisor(aCellPos, 1);
|
|
755
|
+
const aColor = gl.getAttribLocation(program, "a_color");
|
|
756
|
+
gl.enableVertexAttribArray(aColor);
|
|
757
|
+
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * FLOAT);
|
|
758
|
+
gl.vertexAttribDivisor(aColor, 1);
|
|
759
|
+
const aTexCoord = gl.getAttribLocation(program, "a_texCoord");
|
|
760
|
+
gl.enableVertexAttribArray(aTexCoord);
|
|
761
|
+
gl.vertexAttribPointer(aTexCoord, 4, gl.FLOAT, false, stride, 6 * FLOAT);
|
|
762
|
+
gl.vertexAttribDivisor(aTexCoord, 1);
|
|
763
|
+
const aGlyphSize = gl.getAttribLocation(program, "a_glyphSize");
|
|
764
|
+
gl.enableVertexAttribArray(aGlyphSize);
|
|
765
|
+
gl.vertexAttribPointer(aGlyphSize, 2, gl.FLOAT, false, stride, 10 * FLOAT);
|
|
766
|
+
gl.vertexAttribDivisor(aGlyphSize, 1);
|
|
767
|
+
}
|
|
768
|
+
// -----------------------------------------------------------------------
|
|
769
|
+
// Instance buffer management
|
|
770
|
+
// -----------------------------------------------------------------------
|
|
771
|
+
ensureInstanceBuffers() {
|
|
772
|
+
if (!this.grid)
|
|
773
|
+
return;
|
|
774
|
+
const totalCells = this.grid.cols * this.grid.rows;
|
|
775
|
+
const neededBg = totalCells * BG_INSTANCE_FLOATS;
|
|
776
|
+
const neededGlyph = totalCells * GLYPH_INSTANCE_FLOATS;
|
|
777
|
+
if (this.bgInstances.length < neededBg) {
|
|
778
|
+
this.bgInstances = new Float32Array(neededBg);
|
|
779
|
+
}
|
|
780
|
+
if (this.glyphInstances.length < neededGlyph) {
|
|
781
|
+
this.glyphInstances = new Float32Array(neededGlyph);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// -----------------------------------------------------------------------
|
|
785
|
+
// Color resolution
|
|
786
|
+
// -----------------------------------------------------------------------
|
|
787
|
+
buildPaletteFloat() {
|
|
788
|
+
this.paletteFloat = this.palette.map((c) => hexToFloat4(c));
|
|
789
|
+
this.themeFgFloat = hexToFloat4(this.theme.foreground);
|
|
790
|
+
this.themeBgFloat = hexToFloat4(this.theme.background);
|
|
791
|
+
this.themeCursorFloat = hexToFloat4(this.theme.cursor);
|
|
792
|
+
}
|
|
793
|
+
resolveColorFloat(colorIdx, isRGB, grid, col, isForeground) {
|
|
794
|
+
if (isRGB) {
|
|
795
|
+
const offset = isForeground ? col : 256 + col;
|
|
796
|
+
const rgb = grid.rgbColors[offset];
|
|
797
|
+
const r = ((rgb >> 16) & 0xff) / 255;
|
|
798
|
+
const g = ((rgb >> 8) & 0xff) / 255;
|
|
799
|
+
const b = (rgb & 0xff) / 255;
|
|
800
|
+
return [r, g, b, 1.0];
|
|
801
|
+
}
|
|
802
|
+
if (isForeground && colorIdx === 7)
|
|
803
|
+
return this.themeFgFloat;
|
|
804
|
+
if (!isForeground && colorIdx === 0)
|
|
805
|
+
return this.themeBgFloat;
|
|
806
|
+
if (colorIdx >= 0 && colorIdx < 256) {
|
|
807
|
+
return this.paletteFloat[colorIdx];
|
|
808
|
+
}
|
|
809
|
+
return isForeground ? this.themeFgFloat : this.themeBgFloat;
|
|
810
|
+
}
|
|
811
|
+
// -----------------------------------------------------------------------
|
|
812
|
+
// Cursor
|
|
813
|
+
// -----------------------------------------------------------------------
|
|
814
|
+
drawHighlights() {
|
|
815
|
+
if (!this.gl || !this.highlights.length)
|
|
816
|
+
return;
|
|
817
|
+
const gl = this.gl;
|
|
818
|
+
if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
|
|
819
|
+
return;
|
|
820
|
+
const hlInstances = [];
|
|
821
|
+
for (const hl of this.highlights) {
|
|
822
|
+
// Current match: orange, other matches: semi-transparent yellow
|
|
823
|
+
const r = hl.isCurrent ? 1.0 : 1.0;
|
|
824
|
+
const g = hl.isCurrent ? 0.647 : 1.0;
|
|
825
|
+
const b = hl.isCurrent ? 0.0 : 0.0;
|
|
826
|
+
const a = hl.isCurrent ? 0.5 : 0.3;
|
|
827
|
+
for (let col = hl.startCol; col <= hl.endCol; col++) {
|
|
828
|
+
hlInstances.push(col, hl.row, r, g, b, a);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (hlInstances.length === 0)
|
|
832
|
+
return;
|
|
833
|
+
const hlData = new Float32Array(hlInstances);
|
|
834
|
+
const hlCount = hlInstances.length / BG_INSTANCE_FLOATS;
|
|
835
|
+
gl.enable(gl.BLEND);
|
|
836
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
837
|
+
gl.useProgram(this.bgProgram);
|
|
838
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), this.canvas?.width ?? 0, this.canvas?.height ?? 0);
|
|
839
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), this.cellWidth * this.dpr, this.cellHeight * this.dpr);
|
|
840
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
|
|
841
|
+
gl.bufferData(gl.ARRAY_BUFFER, hlData, gl.DYNAMIC_DRAW);
|
|
842
|
+
gl.bindVertexArray(this.bgVAO);
|
|
843
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, hlCount);
|
|
844
|
+
gl.disable(gl.BLEND);
|
|
845
|
+
}
|
|
846
|
+
drawSelection() {
|
|
847
|
+
if (!this.gl || !this.grid || !this.selection)
|
|
848
|
+
return;
|
|
849
|
+
const gl = this.gl;
|
|
850
|
+
const grid = this.grid;
|
|
851
|
+
const sel = normalizeSelection(this.selection);
|
|
852
|
+
const sr = Math.max(0, sel.startRow);
|
|
853
|
+
const er = Math.min(grid.rows - 1, sel.endRow);
|
|
854
|
+
// Skip if selection is empty (same cell)
|
|
855
|
+
if (sr === er && sel.startCol === sel.endCol)
|
|
856
|
+
return;
|
|
857
|
+
if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
|
|
858
|
+
return;
|
|
859
|
+
// Parse the selection background color
|
|
860
|
+
const selColor = hexToFloat4(this.theme.selectionBackground);
|
|
861
|
+
// Build instance data for selection rects
|
|
862
|
+
const selInstances = [];
|
|
863
|
+
for (let row = sr; row <= er; row++) {
|
|
864
|
+
let colStart;
|
|
865
|
+
let colEnd;
|
|
866
|
+
if (sr === er) {
|
|
867
|
+
colStart = sel.startCol;
|
|
868
|
+
colEnd = sel.endCol;
|
|
869
|
+
}
|
|
870
|
+
else if (row === sr) {
|
|
871
|
+
colStart = sel.startCol;
|
|
872
|
+
colEnd = grid.cols - 1;
|
|
873
|
+
}
|
|
874
|
+
else if (row === er) {
|
|
875
|
+
colStart = 0;
|
|
876
|
+
colEnd = sel.endCol;
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
colStart = 0;
|
|
880
|
+
colEnd = grid.cols - 1;
|
|
881
|
+
}
|
|
882
|
+
for (let col = colStart; col <= colEnd; col++) {
|
|
883
|
+
selInstances.push(col, row, selColor[0], selColor[1], selColor[2], 0.5);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (selInstances.length === 0)
|
|
887
|
+
return;
|
|
888
|
+
const selData = new Float32Array(selInstances);
|
|
889
|
+
const selCount = selInstances.length / BG_INSTANCE_FLOATS;
|
|
890
|
+
gl.enable(gl.BLEND);
|
|
891
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
892
|
+
gl.useProgram(this.bgProgram);
|
|
893
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), this.canvas?.width ?? 0, this.canvas?.height ?? 0);
|
|
894
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), this.cellWidth * this.dpr, this.cellHeight * this.dpr);
|
|
895
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
|
|
896
|
+
gl.bufferData(gl.ARRAY_BUFFER, selData, gl.DYNAMIC_DRAW);
|
|
897
|
+
gl.bindVertexArray(this.bgVAO);
|
|
898
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, selCount);
|
|
899
|
+
gl.disable(gl.BLEND);
|
|
900
|
+
}
|
|
901
|
+
drawCursor() {
|
|
902
|
+
if (!this.gl || !this.cursor || !this.cursor.visible)
|
|
903
|
+
return;
|
|
904
|
+
const gl = this.gl;
|
|
905
|
+
const cursor = this.cursor;
|
|
906
|
+
const cellW = this.cellWidth * this.dpr;
|
|
907
|
+
const cellH = this.cellHeight * this.dpr;
|
|
908
|
+
const cc = this.themeCursorFloat;
|
|
909
|
+
// Use the bg program to draw a simple colored rect for the cursor
|
|
910
|
+
if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
|
|
911
|
+
return;
|
|
912
|
+
gl.enable(gl.BLEND);
|
|
913
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
914
|
+
gl.useProgram(this.bgProgram);
|
|
915
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_resolution"), this.canvas?.width ?? 0, this.canvas?.height ?? 0);
|
|
916
|
+
gl.uniform2f(gl.getUniformLocation(this.bgProgram, "u_cellSize"), cellW, cellH);
|
|
917
|
+
// For bar and underline styles, we draw a thin rect.
|
|
918
|
+
// We abuse cellPos with fractional values to position correctly.
|
|
919
|
+
let cursorData;
|
|
920
|
+
switch (cursor.style) {
|
|
921
|
+
case "block":
|
|
922
|
+
cursorData = new Float32Array([
|
|
923
|
+
cursor.col,
|
|
924
|
+
cursor.row,
|
|
925
|
+
cc[0],
|
|
926
|
+
cc[1],
|
|
927
|
+
cc[2],
|
|
928
|
+
0.5, // 50% alpha for block
|
|
929
|
+
]);
|
|
930
|
+
break;
|
|
931
|
+
case "underline": {
|
|
932
|
+
// Draw a thin line at the bottom of the cell
|
|
933
|
+
// We position it by adjusting cellPos row to be near bottom
|
|
934
|
+
const lineH = Math.max(2 * this.dpr, 1);
|
|
935
|
+
const fractionalRow = cursor.row + (cellH - lineH) / cellH;
|
|
936
|
+
cursorData = new Float32Array([cursor.col, fractionalRow, cc[0], cc[1], cc[2], cc[3]]);
|
|
937
|
+
// We'd need a different cell size for this, but we can approximate
|
|
938
|
+
// by using the full cell width and adjusting position
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
case "bar": {
|
|
942
|
+
// Draw a thin vertical bar at the left of the cell
|
|
943
|
+
cursorData = new Float32Array([cursor.col, cursor.row, cc[0], cc[1], cc[2], cc[3]]);
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
default:
|
|
947
|
+
cursorData = new Float32Array([cursor.col, cursor.row, cc[0], cc[1], cc[2], 0.5]);
|
|
948
|
+
}
|
|
949
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
|
|
950
|
+
gl.bufferData(gl.ARRAY_BUFFER, cursorData, gl.DYNAMIC_DRAW);
|
|
951
|
+
gl.bindVertexArray(this.bgVAO);
|
|
952
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, 1);
|
|
953
|
+
gl.disable(gl.BLEND);
|
|
954
|
+
}
|
|
955
|
+
// -----------------------------------------------------------------------
|
|
956
|
+
// Internal helpers
|
|
957
|
+
// -----------------------------------------------------------------------
|
|
958
|
+
measureCellSize() {
|
|
959
|
+
const offscreen = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(100, 100) : null;
|
|
960
|
+
let measureCtx = null;
|
|
961
|
+
if (offscreen) {
|
|
962
|
+
measureCtx = offscreen.getContext("2d");
|
|
963
|
+
}
|
|
964
|
+
else if (typeof document !== "undefined") {
|
|
965
|
+
const tmpCanvas = document.createElement("canvas");
|
|
966
|
+
tmpCanvas.width = 100;
|
|
967
|
+
tmpCanvas.height = 100;
|
|
968
|
+
measureCtx = tmpCanvas.getContext("2d");
|
|
969
|
+
}
|
|
970
|
+
if (!measureCtx) {
|
|
971
|
+
this.cellWidth = Math.ceil(this.fontSize * 0.6);
|
|
972
|
+
this.cellHeight = Math.ceil(this.fontSize * 1.2);
|
|
973
|
+
this.baselineOffset = Math.ceil(this.fontSize);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const font = this.buildFontString(false, false);
|
|
977
|
+
measureCtx.font = font;
|
|
978
|
+
const metrics = measureCtx.measureText("M");
|
|
979
|
+
this.cellWidth = Math.ceil(metrics.width);
|
|
980
|
+
if (typeof metrics.fontBoundingBoxAscent === "number" &&
|
|
981
|
+
typeof metrics.fontBoundingBoxDescent === "number") {
|
|
982
|
+
const ascent = metrics.fontBoundingBoxAscent;
|
|
983
|
+
const descent = metrics.fontBoundingBoxDescent;
|
|
984
|
+
this.cellHeight = Math.ceil(ascent + descent);
|
|
985
|
+
this.baselineOffset = Math.ceil(ascent);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
this.cellHeight = Math.ceil(this.fontSize * 1.2);
|
|
989
|
+
this.baselineOffset = Math.ceil(this.fontSize);
|
|
990
|
+
}
|
|
991
|
+
if (this.cellWidth <= 0)
|
|
992
|
+
this.cellWidth = Math.ceil(this.fontSize * 0.6);
|
|
993
|
+
if (this.cellHeight <= 0)
|
|
994
|
+
this.cellHeight = Math.ceil(this.fontSize * 1.2);
|
|
995
|
+
}
|
|
996
|
+
syncCanvasSize() {
|
|
997
|
+
if (!this.canvas || !this.grid)
|
|
998
|
+
return;
|
|
999
|
+
const { cols, rows } = this.grid;
|
|
1000
|
+
const width = cols * this.cellWidth;
|
|
1001
|
+
const height = rows * this.cellHeight;
|
|
1002
|
+
this.canvas.width = Math.round(width * this.dpr);
|
|
1003
|
+
this.canvas.height = Math.round(height * this.dpr);
|
|
1004
|
+
this.canvas.style.width = `${width}px`;
|
|
1005
|
+
this.canvas.style.height = `${height}px`;
|
|
1006
|
+
}
|
|
1007
|
+
buildFontString(bold, italic) {
|
|
1008
|
+
let font = "";
|
|
1009
|
+
if (italic)
|
|
1010
|
+
font += "italic ";
|
|
1011
|
+
if (bold)
|
|
1012
|
+
font += "bold ";
|
|
1013
|
+
font += `${this.fontSize}px ${this.fontFamily}`;
|
|
1014
|
+
return font;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
// ---------------------------------------------------------------------------
|
|
1018
|
+
// Factory function with fallback
|
|
1019
|
+
// ---------------------------------------------------------------------------
|
|
1020
|
+
/**
|
|
1021
|
+
* Create a renderer with the given strategy.
|
|
1022
|
+
*
|
|
1023
|
+
* - 'auto' (default): try WebGL2 first, fall back to Canvas 2D
|
|
1024
|
+
* - 'webgl': force WebGL2 (throws if unavailable)
|
|
1025
|
+
* - 'canvas2d': force Canvas 2D
|
|
1026
|
+
*/
|
|
1027
|
+
export function createRenderer(options, type = "auto") {
|
|
1028
|
+
if (type === "canvas2d") {
|
|
1029
|
+
return new Canvas2DRenderer(options);
|
|
1030
|
+
}
|
|
1031
|
+
if (type === "webgl") {
|
|
1032
|
+
return new WebGLRenderer(options);
|
|
1033
|
+
}
|
|
1034
|
+
// 'auto': try WebGL2 first
|
|
1035
|
+
// We can't easily test WebGL2 availability without a canvas,
|
|
1036
|
+
// so we create the WebGLRenderer and let attach() throw if unavailable.
|
|
1037
|
+
// Instead, probe with a temporary canvas.
|
|
1038
|
+
if (typeof document !== "undefined") {
|
|
1039
|
+
const testCanvas = document.createElement("canvas");
|
|
1040
|
+
const testGl = testCanvas.getContext("webgl2");
|
|
1041
|
+
if (testGl) {
|
|
1042
|
+
return new WebGLRenderer(options);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return new Canvas2DRenderer(options);
|
|
1046
|
+
}
|
|
1047
|
+
//# sourceMappingURL=webgl-renderer.js.map
|