@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.
Files changed (61) hide show
  1. package/dist/accessibility.d.ts +46 -0
  2. package/dist/accessibility.d.ts.map +1 -0
  3. package/dist/accessibility.js +196 -0
  4. package/dist/accessibility.js.map +1 -0
  5. package/dist/addon.d.ts.map +1 -0
  6. package/dist/addon.js +2 -0
  7. package/dist/addon.js.map +1 -0
  8. package/dist/addons/fit.d.ts.map +1 -0
  9. package/dist/addons/fit.js +40 -0
  10. package/dist/addons/fit.js.map +1 -0
  11. package/dist/addons/search.d.ts +56 -0
  12. package/dist/addons/search.d.ts.map +1 -0
  13. package/dist/addons/search.js +178 -0
  14. package/dist/addons/search.js.map +1 -0
  15. package/dist/addons/web-links.d.ts +30 -0
  16. package/dist/addons/web-links.d.ts.map +1 -0
  17. package/dist/addons/web-links.js +170 -0
  18. package/dist/addons/web-links.js.map +1 -0
  19. package/dist/fit.d.ts.map +1 -0
  20. package/dist/fit.js +14 -0
  21. package/dist/fit.js.map +1 -0
  22. package/dist/index.d.ts +24 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +14 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/input-handler.d.ts +185 -0
  27. package/dist/input-handler.d.ts.map +1 -0
  28. package/dist/input-handler.js +1197 -0
  29. package/dist/input-handler.js.map +1 -0
  30. package/dist/parser-worker.d.ts.map +1 -0
  31. package/dist/parser-worker.js +128 -0
  32. package/dist/parser-worker.js.map +1 -0
  33. package/dist/render-bridge.d.ts +56 -0
  34. package/dist/render-bridge.d.ts.map +1 -0
  35. package/dist/render-bridge.js +158 -0
  36. package/dist/render-bridge.js.map +1 -0
  37. package/dist/render-worker.d.ts +62 -0
  38. package/dist/render-worker.d.ts.map +1 -0
  39. package/dist/render-worker.js +720 -0
  40. package/dist/render-worker.js.map +1 -0
  41. package/dist/renderer.d.ts +86 -0
  42. package/dist/renderer.d.ts.map +1 -0
  43. package/dist/renderer.js +454 -0
  44. package/dist/renderer.js.map +1 -0
  45. package/dist/shared-context.d.ts +93 -0
  46. package/dist/shared-context.d.ts.map +1 -0
  47. package/dist/shared-context.js +561 -0
  48. package/dist/shared-context.js.map +1 -0
  49. package/dist/web-terminal.d.ts +152 -0
  50. package/dist/web-terminal.d.ts.map +1 -0
  51. package/dist/web-terminal.js +684 -0
  52. package/dist/web-terminal.js.map +1 -0
  53. package/dist/webgl-renderer.d.ts +146 -0
  54. package/dist/webgl-renderer.d.ts.map +1 -0
  55. package/dist/webgl-renderer.js +1047 -0
  56. package/dist/webgl-renderer.js.map +1 -0
  57. package/dist/worker-bridge.d.ts +51 -0
  58. package/dist/worker-bridge.d.ts.map +1 -0
  59. package/dist/worker-bridge.js +185 -0
  60. package/dist/worker-bridge.js.map +1 -0
  61. 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