@next_term/web 0.1.0-next.0 → 0.1.0-next.10

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/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/accessibility.d.ts +2 -4
  4. package/dist/accessibility.d.ts.map +1 -1
  5. package/dist/accessibility.js +2 -7
  6. package/dist/accessibility.js.map +1 -1
  7. package/dist/addon.d.ts +9 -0
  8. package/dist/addons/fit.d.ts +23 -0
  9. package/dist/addons/web-links.d.ts +0 -1
  10. package/dist/addons/web-links.d.ts.map +1 -1
  11. package/dist/addons/web-links.js +0 -4
  12. package/dist/addons/web-links.js.map +1 -1
  13. package/dist/fit.d.ts +9 -0
  14. package/dist/fit.d.ts.map +1 -1
  15. package/dist/fit.js +9 -1
  16. package/dist/fit.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/input-handler.d.ts +7 -0
  22. package/dist/input-handler.d.ts.map +1 -1
  23. package/dist/input-handler.js +34 -5
  24. package/dist/input-handler.js.map +1 -1
  25. package/dist/parser-worker.d.ts +34 -0
  26. package/dist/parser-worker.d.ts.map +1 -1
  27. package/dist/parser-worker.js +4 -17
  28. package/dist/parser-worker.js.map +1 -1
  29. package/dist/render-bridge.d.ts +3 -1
  30. package/dist/render-bridge.d.ts.map +1 -1
  31. package/dist/render-bridge.js +5 -1
  32. package/dist/render-bridge.js.map +1 -1
  33. package/dist/render-worker.d.ts +4 -0
  34. package/dist/render-worker.d.ts.map +1 -1
  35. package/dist/render-worker.js +211 -98
  36. package/dist/render-worker.js.map +1 -1
  37. package/dist/renderer.d.ts +7 -1
  38. package/dist/renderer.d.ts.map +1 -1
  39. package/dist/renderer.js +21 -6
  40. package/dist/renderer.js.map +1 -1
  41. package/dist/shared-context.d.ts +38 -9
  42. package/dist/shared-context.d.ts.map +1 -1
  43. package/dist/shared-context.js +491 -193
  44. package/dist/shared-context.js.map +1 -1
  45. package/dist/web-terminal.d.ts +26 -1
  46. package/dist/web-terminal.d.ts.map +1 -1
  47. package/dist/web-terminal.js +140 -18
  48. package/dist/web-terminal.js.map +1 -1
  49. package/dist/webgl-renderer.d.ts +33 -7
  50. package/dist/webgl-renderer.d.ts.map +1 -1
  51. package/dist/webgl-renderer.js +372 -139
  52. package/dist/webgl-renderer.js.map +1 -1
  53. package/dist/webgl-utils.d.ts +4 -0
  54. package/dist/webgl-utils.d.ts.map +1 -0
  55. package/dist/webgl-utils.js +19 -0
  56. package/dist/webgl-utils.js.map +1 -0
  57. package/dist/worker-bridge.d.ts +7 -2
  58. package/dist/worker-bridge.d.ts.map +1 -1
  59. package/dist/worker-bridge.js +41 -13
  60. package/dist/worker-bridge.js.map +1 -1
  61. package/package.json +6 -6
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { DEFAULT_THEME, normalizeSelection } from "@next_term/core";
10
10
  import { build256Palette, Canvas2DRenderer } from "./renderer.js";
11
+ import { resolveColorFloat } from "./webgl-utils.js";
11
12
  // ---------------------------------------------------------------------------
12
13
  // Attribute bit positions (mirrors renderer.ts / cell-grid.ts)
13
14
  // ---------------------------------------------------------------------------
@@ -19,31 +20,82 @@ const ATTR_INVERSE = 0x40;
19
20
  // ---------------------------------------------------------------------------
20
21
  // Color helpers
21
22
  // ---------------------------------------------------------------------------
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;
23
+ /**
24
+ * Parse any CSS color string to [r, g, b, a] in 0-1 range.
25
+ *
26
+ * Fast path for #rrggbb/#rgb hex (no canvas overhead).
27
+ * All other formats (rgb(), rgba(), hsl(), oklch(), color(), named colors)
28
+ * are resolved by the browser's native CSS engine via a 1x1 canvas.
29
+ */
30
+ // Singleton canvas context for CSS color resolution (works in main thread
31
+ // and Web Workers via OffscreenCanvas)
32
+ let _colorCtx = null;
33
+ let _colorCtxFailed = false;
34
+ function getColorCtx() {
35
+ if (_colorCtx || _colorCtxFailed)
36
+ return _colorCtx;
37
+ try {
38
+ if (typeof OffscreenCanvas !== "undefined") {
39
+ _colorCtx = new OffscreenCanvas(1, 1).getContext("2d");
31
40
  }
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;
41
+ else if (typeof document !== "undefined") {
42
+ const c = document.createElement("canvas");
43
+ c.width = 1;
44
+ c.height = 1;
45
+ _colorCtx = c.getContext("2d");
36
46
  }
37
47
  }
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;
48
+ catch {
49
+ // No canvas available (SSR / test environment)
50
+ }
51
+ if (!_colorCtx)
52
+ _colorCtxFailed = true;
53
+ return _colorCtx;
54
+ }
55
+ export function hexToFloat4(color) {
56
+ // Fast path: #rrggbb (most common — default theme + 256-palette are all hex)
57
+ if (color.length === 7 && color.charCodeAt(0) === 0x23 /* # */) {
58
+ return [
59
+ parseInt(color.slice(1, 3), 16) / 255,
60
+ parseInt(color.slice(3, 5), 16) / 255,
61
+ parseInt(color.slice(5, 7), 16) / 255,
62
+ 1.0,
63
+ ];
64
+ }
65
+ // Fast path: #rgb
66
+ if (color.length === 4 && color.charCodeAt(0) === 0x23) {
67
+ return [
68
+ parseInt(color[1] + color[1], 16) / 255,
69
+ parseInt(color[2] + color[2], 16) / 255,
70
+ parseInt(color[3] + color[3], 16) / 255,
71
+ 1.0,
72
+ ];
73
+ }
74
+ // Universal path: let the browser parse any CSS color
75
+ const ctx = getColorCtx();
76
+ if (ctx) {
77
+ try {
78
+ ctx.clearRect(0, 0, 1, 1);
79
+ ctx.fillStyle = "#000";
80
+ ctx.fillStyle = color;
81
+ ctx.fillRect(0, 0, 1, 1);
82
+ const d = ctx.getImageData(0, 0, 1, 1).data;
83
+ const a = d[3] / 255;
84
+ if (a === 0) {
85
+ // Invalid color or fully transparent — return opaque black
86
+ return [0, 0, 0, 1.0];
87
+ }
88
+ // Unpremultiply RGB (getImageData returns premultiplied on some browsers)
89
+ return a >= 1
90
+ ? [d[0] / 255, d[1] / 255, d[2] / 255, 1.0]
91
+ : [d[0] / 255 / a, d[1] / 255 / a, d[2] / 255 / a, a];
92
+ }
93
+ catch {
94
+ // Partial canvas implementation (e.g., jsdom) — fall through
44
95
  }
45
96
  }
46
- return [r, g, b, 1.0];
97
+ // No canvas fallback — return black
98
+ return [0, 0, 0, 1.0];
47
99
  }
48
100
  /** Build a glyph cache key from codepoint and style flags. */
49
101
  export function glyphCacheKey(codepoint, bold, italic) {
@@ -65,9 +117,13 @@ export class GlyphAtlas {
65
117
  dirty = false;
66
118
  fontSize;
67
119
  fontFamily;
68
- constructor(fontSize, fontFamily, initialSize = 512) {
120
+ fontWeight;
121
+ fontWeightBold;
122
+ constructor(fontSize, fontFamily, fontWeight = 400, fontWeightBold = 700, initialSize = 512) {
69
123
  this.fontSize = fontSize;
70
124
  this.fontFamily = fontFamily;
125
+ this.fontWeight = fontWeight;
126
+ this.fontWeightBold = fontWeightBold;
71
127
  this.width = initialSize;
72
128
  this.height = initialSize;
73
129
  if (typeof OffscreenCanvas !== "undefined") {
@@ -78,6 +134,21 @@ export class GlyphAtlas {
78
134
  this.ctx = ctx;
79
135
  }
80
136
  }
137
+ /**
138
+ * Clear the glyph cache so all glyphs are re-rasterized on next access.
139
+ * Used when the underlying font changes (e.g., after a web font loads).
140
+ */
141
+ clearCache() {
142
+ this.cache.clear();
143
+ this.nextX = 0;
144
+ this.nextY = 0;
145
+ this.rowHeight = 0;
146
+ this.dirty = true;
147
+ // Clear the atlas canvas
148
+ if (this.ctx && this.canvas) {
149
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
150
+ }
151
+ }
81
152
  /**
82
153
  * Get (or rasterize) a glyph. Returns the GlyphInfo with atlas coordinates.
83
154
  */
@@ -173,8 +244,7 @@ export class GlyphAtlas {
173
244
  let font = "";
174
245
  if (italic)
175
246
  font += "italic ";
176
- if (bold)
177
- font += "bold ";
247
+ font += `${bold ? this.fontWeightBold : this.fontWeight} `;
178
248
  font += `${this.fontSize}px ${this.fontFamily}`;
179
249
  return font;
180
250
  }
@@ -361,9 +431,12 @@ export class WebGLRenderer {
361
431
  cursor = null;
362
432
  cellWidth = 0;
363
433
  cellHeight = 0;
434
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: read via destructuring in render()
364
435
  baselineOffset = 0;
365
436
  fontSize;
366
437
  fontFamily;
438
+ fontWeight;
439
+ fontWeightBold;
367
440
  theme;
368
441
  dpr;
369
442
  palette;
@@ -380,15 +453,36 @@ export class WebGLRenderer {
380
453
  glyphProgram = null;
381
454
  quadVBO = null;
382
455
  quadEBO = null;
383
- bgInstanceVBO = null;
384
- glyphInstanceVBO = null;
456
+ // Double-buffered instance VBOs to avoid GPU read/write conflicts
457
+ bgInstanceVBOs = [null, null];
458
+ glyphInstanceVBOs = [null, null];
459
+ activeBufferIdx = 0;
460
+ // Dedicated overlay VBO for cursor/selection/highlights so we don't
461
+ // overwrite the active bg VBO and neutralize double-buffering.
462
+ overlayVBO = null;
385
463
  bgVAO = null;
386
464
  glyphVAO = null;
465
+ // Cached uniform locations (populated in initGLResources)
466
+ bgResolutionLoc = null;
467
+ bgCellSizeLoc = null;
468
+ glyphResolutionLoc = null;
469
+ glyphCellSizeLoc = null;
470
+ glyphAtlasLoc = null;
387
471
  // Instance data (CPU side)
388
472
  bgInstances;
389
473
  glyphInstances;
390
474
  bgCount = 0;
391
475
  glyphCount = 0;
476
+ // Per-row dirty tracking for incremental instance rebuilds
477
+ rowBgOffsets = []; // starting bgCount index per row
478
+ rowBgCounts = []; // number of bg instances per row
479
+ rowGlyphOffsets = []; // starting glyphCount index per row
480
+ rowGlyphCounts = []; // number of glyph instances per row
481
+ hasRenderedOnce = false;
482
+ // Pre-allocated overlay buffers (reused each frame)
483
+ cursorData = new Float32Array(BG_INSTANCE_FLOATS);
484
+ selBuffer = new Float32Array(256 * BG_INSTANCE_FLOATS);
485
+ hlBuffer = new Float32Array(256 * BG_INSTANCE_FLOATS);
392
486
  // Glyph atlas
393
487
  atlas;
394
488
  // Palette as float arrays (cached for performance)
@@ -402,13 +496,15 @@ export class WebGLRenderer {
402
496
  constructor(options) {
403
497
  this.fontSize = options.fontSize;
404
498
  this.fontFamily = options.fontFamily;
499
+ this.fontWeight = options.fontWeight ?? 400;
500
+ this.fontWeightBold = options.fontWeightBold ?? 700;
405
501
  this.theme = options.theme ?? DEFAULT_THEME;
406
502
  this.dpr =
407
503
  options.devicePixelRatio ?? (typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1);
408
504
  this.palette = build256Palette(this.theme);
409
505
  this.measureCellSize();
410
506
  this.buildPaletteFloat();
411
- this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily);
507
+ this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily, this.fontWeight, this.fontWeightBold);
412
508
  // Pre-allocate instance buffers for a reasonable default size
413
509
  const maxCells = 80 * 24;
414
510
  this.bgInstances = new Float32Array(maxCells * BG_INSTANCE_FLOATS);
@@ -421,6 +517,7 @@ export class WebGLRenderer {
421
517
  this.canvas = canvas;
422
518
  this.grid = grid;
423
519
  this.cursor = cursor;
520
+ this.hasRenderedOnce = false;
424
521
  // Get WebGL2 context
425
522
  this.gl = canvas.getContext("webgl2", {
426
523
  alpha: false,
@@ -482,10 +579,38 @@ export class WebGLRenderer {
482
579
  if (!anyDirty) {
483
580
  return;
484
581
  }
485
- // Rebuild instance data
486
- this.bgCount = 0;
487
- this.glyphCount = 0;
582
+ // Flip active double-buffer index
583
+ this.activeBufferIdx = 1 - this.activeBufferIdx;
584
+ // Incremental rebuild — only re-pack dirty rows
585
+ // On first render or if grid dimensions changed, initialize per-row tracking
586
+ if (!this.hasRenderedOnce || this.rowBgOffsets.length !== rows) {
587
+ this.rowBgOffsets = new Array(rows).fill(0);
588
+ this.rowBgCounts = new Array(rows).fill(0);
589
+ this.rowGlyphOffsets = new Array(rows).fill(0);
590
+ this.rowGlyphCounts = new Array(rows).fill(0);
591
+ // Compute fixed offsets: each row has exactly `cols` bg instances
592
+ // Glyph offsets are variable, so on first pass we do a full rebuild
593
+ let bgOff = 0;
594
+ let glyphOff = 0;
595
+ for (let r = 0; r < rows; r++) {
596
+ this.rowBgOffsets[r] = bgOff;
597
+ this.rowBgCounts[r] = cols; // one bg instance per cell
598
+ bgOff += cols;
599
+ // For glyphs, allocate max possible (cols) per row on first pass
600
+ this.rowGlyphOffsets[r] = glyphOff;
601
+ this.rowGlyphCounts[r] = 0;
602
+ glyphOff += cols;
603
+ }
604
+ this.bgCount = bgOff;
605
+ this.glyphCount = 0; // will be summed below
606
+ }
488
607
  for (let row = 0; row < rows; row++) {
608
+ // Skip non-dirty rows — their data persists in the arrays
609
+ if (!grid.isDirty(row))
610
+ continue;
611
+ const bgBase = this.rowBgOffsets[row] * BG_INSTANCE_FLOATS;
612
+ const glyphBase = this.rowGlyphOffsets[row] * GLYPH_INSTANCE_FLOATS;
613
+ let rowGlyphCount = 0;
489
614
  for (let col = 0; col < cols; col++) {
490
615
  const codepoint = grid.getCodepoint(row, col);
491
616
  const fgIdx = grid.getFgIndex(row, col);
@@ -493,33 +618,59 @@ export class WebGLRenderer {
493
618
  const attrs = grid.getAttrs(row, col);
494
619
  const fgIsRGB = grid.isFgRGB(row, col);
495
620
  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);
621
+ const wide = grid.isWide(row, col);
622
+ // Skip spacer cells (right half of wide character)
623
+ if (grid.isSpacerCell(row, col)) {
624
+ // Still need to emit a bg instance for this column
625
+ packBgInstance(this.bgInstances, bgBase + col * BG_INSTANCE_FLOATS, col, row, 0, 0, 0, 0);
626
+ continue;
627
+ }
628
+ let fg = resolveColorFloat(fgIdx, fgIsRGB, grid, col, true, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
629
+ let bg = resolveColorFloat(bgIdx, bgIsRGB, grid, col, false, this.paletteFloat, this.themeFgFloat, this.themeBgFloat);
499
630
  // Handle inverse
500
631
  if (attrs & ATTR_INVERSE) {
501
632
  const tmp = fg;
502
633
  fg = bg;
503
634
  bg = tmp;
504
635
  }
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++;
636
+ // Background instance — wide chars get 2x width via two bg cells
637
+ packBgInstance(this.bgInstances, bgBase + col * BG_INSTANCE_FLOATS, col, row, bg[0], bg[1], bg[2], bg[3]);
638
+ if (wide && col + 1 < cols) {
639
+ // Paint right-half bg with same color
640
+ packBgInstance(this.bgInstances, bgBase + (col + 1) * BG_INSTANCE_FLOATS, col + 1, row, bg[0], bg[1], bg[2], bg[3]);
641
+ }
508
642
  // Glyph instance — skip spaces and control chars
509
643
  if (codepoint > 0x20) {
510
644
  const bold = !!(attrs & ATTR_BOLD);
511
645
  const italic = !!(attrs & ATTR_ITALIC);
512
646
  const glyph = this.atlas.getGlyph(codepoint, bold, italic);
513
647
  if (glyph) {
514
- const glyphPw = isWide ? glyph.pw : glyph.pw;
515
648
  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++;
649
+ packGlyphInstance(this.glyphInstances, glyphBase + rowGlyphCount * GLYPH_INSTANCE_FLOATS, col, row, fg[0], fg[1], fg[2], fg[3], glyph.u, glyph.v, glyph.w, glyph.h, glyph.pw, glyphPh);
650
+ rowGlyphCount++;
518
651
  }
519
652
  }
520
653
  }
654
+ // Zero out remaining glyph slots for this row (if fewer glyphs than last time)
655
+ const maxGlyphSlots = cols;
656
+ for (let i = rowGlyphCount; i < this.rowGlyphCounts[row]; i++) {
657
+ // Zero the codepoint/color to make invisible (alpha=0 effectively)
658
+ const off = glyphBase + i * GLYPH_INSTANCE_FLOATS;
659
+ for (let j = 0; j < GLYPH_INSTANCE_FLOATS; j++) {
660
+ this.glyphInstances[off + j] = 0;
661
+ }
662
+ }
663
+ // Keep max of old and new count to ensure we still upload zeroed slots
664
+ if (rowGlyphCount > maxGlyphSlots)
665
+ rowGlyphCount = maxGlyphSlots;
666
+ this.rowGlyphCounts[row] = rowGlyphCount;
521
667
  grid.clearDirty(row);
522
668
  }
669
+ this.hasRenderedOnce = true;
670
+ // Both bg and glyph instance arrays are sized for rows * cols slots;
671
+ // data is packed per-row at fixed offsets so we always upload the full region.
672
+ this.bgCount = rows * cols;
673
+ this.glyphCount = rows * cols;
523
674
  // Upload atlas if dirty
524
675
  this.atlas.upload(gl);
525
676
  // Set up GL state
@@ -530,39 +681,54 @@ export class WebGLRenderer {
530
681
  gl.clear(gl.COLOR_BUFFER_BIT);
531
682
  const cellW = this.cellWidth * this.dpr;
532
683
  const cellH = this.cellHeight * this.dpr;
684
+ const _FLOAT = 4;
685
+ const activeBgVBO = this.bgInstanceVBOs[this.activeBufferIdx];
686
+ const activeGlyphVBO = this.glyphInstanceVBOs[this.activeBufferIdx];
533
687
  // --- Background pass ---
534
- if (this.bgCount > 0 && this.bgProgram && this.bgVAO && this.bgInstanceVBO) {
688
+ if (this.bgCount > 0 && this.bgProgram && this.bgVAO && activeBgVBO) {
535
689
  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);
690
+ // Use cached uniform locations
691
+ gl.uniform2f(this.bgResolutionLoc, canvasWidth, canvasHeight);
692
+ gl.uniform2f(this.bgCellSizeLoc, cellW, cellH);
693
+ // Bind active double-buffered VBO and re-setup instance attrib pointers
694
+ gl.bindBuffer(gl.ARRAY_BUFFER, activeBgVBO);
695
+ gl.bufferData(gl.ARRAY_BUFFER, this.bgInstances.subarray(0, this.bgCount * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
540
696
  gl.bindVertexArray(this.bgVAO);
697
+ // Rebind instance attribs to the active VBO (VAO captured the old one)
698
+ this.rebindBgInstanceAttribs(gl, activeBgVBO);
541
699
  gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.bgCount);
542
700
  }
543
701
  // --- Glyph pass ---
544
- if (this.glyphCount > 0 && this.glyphProgram && this.glyphVAO && this.glyphInstanceVBO) {
702
+ if (this.glyphCount > 0 && this.glyphProgram && this.glyphVAO && activeGlyphVBO) {
545
703
  gl.enable(gl.BLEND);
546
704
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
547
705
  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);
706
+ // Use cached uniform locations
707
+ gl.uniform2f(this.glyphResolutionLoc, canvasWidth, canvasHeight);
708
+ gl.uniform2f(this.glyphCellSizeLoc, cellW, cellH);
550
709
  // Bind atlas texture
551
710
  gl.activeTexture(gl.TEXTURE0);
552
711
  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);
712
+ gl.uniform1i(this.glyphAtlasLoc, 0);
713
+ // Bind active double-buffered VBO
714
+ gl.bindBuffer(gl.ARRAY_BUFFER, activeGlyphVBO);
715
+ gl.bufferData(gl.ARRAY_BUFFER, this.glyphInstances.subarray(0, this.glyphCount * GLYPH_INSTANCE_FLOATS), gl.STREAM_DRAW);
556
716
  gl.bindVertexArray(this.glyphVAO);
717
+ // Rebind instance attribs to the active VBO
718
+ this.rebindGlyphInstanceAttribs(gl, activeGlyphVBO);
557
719
  gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, this.glyphCount);
558
720
  gl.disable(gl.BLEND);
559
721
  }
722
+ // Enable BLEND once for all overlay passes
723
+ gl.enable(gl.BLEND);
724
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
560
725
  // --- Highlights (search results) ---
561
726
  this.drawHighlights();
562
727
  // --- Selection overlay ---
563
728
  this.drawSelection();
564
729
  // --- Cursor ---
565
730
  this.drawCursor();
731
+ gl.disable(gl.BLEND);
566
732
  gl.bindVertexArray(null);
567
733
  }
568
734
  resize(_cols, _rows) {
@@ -592,15 +758,19 @@ export class WebGLRenderer {
592
758
  this.grid.markAllDirty();
593
759
  }
594
760
  }
595
- setFont(fontSize, fontFamily) {
761
+ setFont(fontSize, fontFamily, fontWeight, fontWeightBold) {
596
762
  this.fontSize = fontSize;
597
763
  this.fontFamily = fontFamily;
764
+ if (fontWeight !== undefined)
765
+ this.fontWeight = fontWeight;
766
+ if (fontWeightBold !== undefined)
767
+ this.fontWeightBold = fontWeightBold;
598
768
  this.measureCellSize();
599
- // Recreate atlas with new font size
769
+ // Recreate atlas with new font size/weight
600
770
  if (this.gl) {
601
771
  this.atlas.dispose(this.gl);
602
772
  }
603
- this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily);
773
+ this.atlas = new GlyphAtlas(Math.round(this.fontSize * this.dpr), this.fontFamily, this.fontWeight, this.fontWeightBold);
604
774
  if (this.grid) {
605
775
  this.syncCanvasSize();
606
776
  this.grid.markAllDirty();
@@ -631,10 +801,16 @@ export class WebGLRenderer {
631
801
  gl.deleteBuffer(this.quadVBO);
632
802
  if (this.quadEBO)
633
803
  gl.deleteBuffer(this.quadEBO);
634
- if (this.bgInstanceVBO)
635
- gl.deleteBuffer(this.bgInstanceVBO);
636
- if (this.glyphInstanceVBO)
637
- gl.deleteBuffer(this.glyphInstanceVBO);
804
+ if (this.bgInstanceVBOs[0])
805
+ gl.deleteBuffer(this.bgInstanceVBOs[0]);
806
+ if (this.bgInstanceVBOs[1])
807
+ gl.deleteBuffer(this.bgInstanceVBOs[1]);
808
+ if (this.glyphInstanceVBOs[0])
809
+ gl.deleteBuffer(this.glyphInstanceVBOs[0]);
810
+ if (this.glyphInstanceVBOs[1])
811
+ gl.deleteBuffer(this.glyphInstanceVBOs[1]);
812
+ if (this.overlayVBO)
813
+ gl.deleteBuffer(this.overlayVBO);
638
814
  if (this.bgVAO)
639
815
  gl.deleteVertexArray(this.bgVAO);
640
816
  if (this.glyphVAO)
@@ -693,19 +869,40 @@ export class WebGLRenderer {
693
869
  this.quadEBO = gl.createBuffer();
694
870
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
695
871
  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
872
+ // Double-buffered instance VBOs
873
+ this.bgInstanceVBOs = [gl.createBuffer(), gl.createBuffer()];
874
+ this.glyphInstanceVBOs = [gl.createBuffer(), gl.createBuffer()];
875
+ // Dedicated overlay VBO for cursor/selection/highlights
876
+ this.overlayVBO = gl.createBuffer();
877
+ // Set up background VAO (quad + EBO only; instance buffer bound per-frame)
700
878
  this.bgVAO = gl.createVertexArray();
701
879
  gl.bindVertexArray(this.bgVAO);
702
880
  this.setupBgVAO(gl);
703
881
  gl.bindVertexArray(null);
704
- // Set up glyph VAO
882
+ // Set up glyph VAO (quad + EBO only; instance buffer bound per-frame)
705
883
  this.glyphVAO = gl.createVertexArray();
706
884
  gl.bindVertexArray(this.glyphVAO);
707
885
  this.setupGlyphVAO(gl);
708
886
  gl.bindVertexArray(null);
887
+ // Cache all uniform locations after programs are compiled
888
+ this.bgResolutionLoc = gl.getUniformLocation(this.bgProgram, "u_resolution");
889
+ this.bgCellSizeLoc = gl.getUniformLocation(this.bgProgram, "u_cellSize");
890
+ this.glyphResolutionLoc = gl.getUniformLocation(this.glyphProgram, "u_resolution");
891
+ this.glyphCellSizeLoc = gl.getUniformLocation(this.glyphProgram, "u_cellSize");
892
+ this.glyphAtlasLoc = gl.getUniformLocation(this.glyphProgram, "u_atlas");
893
+ // Cache attribute locations
894
+ this.bgAttribLocs = {
895
+ cellPos: gl.getAttribLocation(this.bgProgram, "a_cellPos"),
896
+ color: gl.getAttribLocation(this.bgProgram, "a_color"),
897
+ };
898
+ this.glyphAttribLocs = {
899
+ cellPos: gl.getAttribLocation(this.glyphProgram, "a_cellPos"),
900
+ color: gl.getAttribLocation(this.glyphProgram, "a_color"),
901
+ texCoord: gl.getAttribLocation(this.glyphProgram, "a_texCoord"),
902
+ glyphSize: gl.getAttribLocation(this.glyphProgram, "a_glyphSize"),
903
+ };
904
+ // Reset dirty-row tracking state on GL reinit
905
+ this.hasRenderedOnce = false;
709
906
  // Recreate atlas texture
710
907
  this.atlas.recreateTexture();
711
908
  }
@@ -721,8 +918,8 @@ export class WebGLRenderer {
721
918
  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
722
919
  // Element buffer
723
920
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
724
- // Instance data
725
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
921
+ // Instance data — bind initial buffer; will be rebound per-frame for double buffering
922
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBOs[0]);
726
923
  const stride = BG_INSTANCE_FLOATS * FLOAT;
727
924
  const aCellPos = gl.getAttribLocation(program, "a_cellPos");
728
925
  gl.enableVertexAttribArray(aCellPos);
@@ -745,8 +942,8 @@ export class WebGLRenderer {
745
942
  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
746
943
  // Element buffer
747
944
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.quadEBO);
748
- // Instance data
749
- gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBO);
945
+ // Instance data — bind initial buffer; will be rebound per-frame for double buffering
946
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphInstanceVBOs[0]);
750
947
  const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
751
948
  const aCellPos = gl.getAttribLocation(program, "a_cellPos");
752
949
  gl.enableVertexAttribArray(aCellPos);
@@ -776,9 +973,11 @@ export class WebGLRenderer {
776
973
  const neededGlyph = totalCells * GLYPH_INSTANCE_FLOATS;
777
974
  if (this.bgInstances.length < neededBg) {
778
975
  this.bgInstances = new Float32Array(neededBg);
976
+ this.hasRenderedOnce = false; // force full rebuild on resize
779
977
  }
780
978
  if (this.glyphInstances.length < neededGlyph) {
781
979
  this.glyphInstances = new Float32Array(neededGlyph);
980
+ this.hasRenderedOnce = false;
782
981
  }
783
982
  }
784
983
  // -----------------------------------------------------------------------
@@ -790,24 +989,6 @@ export class WebGLRenderer {
790
989
  this.themeBgFloat = hexToFloat4(this.theme.background);
791
990
  this.themeCursorFloat = hexToFloat4(this.theme.cursor);
792
991
  }
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
992
  // -----------------------------------------------------------------------
812
993
  // Cursor
813
994
  // -----------------------------------------------------------------------
@@ -815,33 +996,44 @@ export class WebGLRenderer {
815
996
  if (!this.gl || !this.highlights.length)
816
997
  return;
817
998
  const gl = this.gl;
818
- if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
999
+ if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
819
1000
  return;
820
- const hlInstances = [];
1001
+ // Pack into pre-allocated hlBuffer, growing only if needed
1002
+ let hlIdx = 0;
821
1003
  for (const hl of this.highlights) {
822
- // Current match: orange, other matches: semi-transparent yellow
823
1004
  const r = hl.isCurrent ? 1.0 : 1.0;
824
1005
  const g = hl.isCurrent ? 0.647 : 1.0;
825
1006
  const b = hl.isCurrent ? 0.0 : 0.0;
826
1007
  const a = hl.isCurrent ? 0.5 : 0.3;
827
1008
  for (let col = hl.startCol; col <= hl.endCol; col++) {
828
- hlInstances.push(col, hl.row, r, g, b, a);
1009
+ const needed = (hlIdx + 1) * BG_INSTANCE_FLOATS;
1010
+ if (needed > this.hlBuffer.length) {
1011
+ const newBuf = new Float32Array(this.hlBuffer.length * 2);
1012
+ newBuf.set(this.hlBuffer);
1013
+ this.hlBuffer = newBuf;
1014
+ }
1015
+ const off = hlIdx * BG_INSTANCE_FLOATS;
1016
+ this.hlBuffer[off] = col;
1017
+ this.hlBuffer[off + 1] = hl.row;
1018
+ this.hlBuffer[off + 2] = r;
1019
+ this.hlBuffer[off + 3] = g;
1020
+ this.hlBuffer[off + 4] = b;
1021
+ this.hlBuffer[off + 5] = a;
1022
+ hlIdx++;
829
1023
  }
830
1024
  }
831
- if (hlInstances.length === 0)
1025
+ if (hlIdx === 0)
832
1026
  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);
1027
+ // BLEND already enabled by caller
837
1028
  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);
1029
+ // Use cached uniform locations
1030
+ gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
1031
+ gl.uniform2f(this.bgCellSizeLoc, this.cellWidth * this.dpr, this.cellHeight * this.dpr);
1032
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
1033
+ gl.bufferData(gl.ARRAY_BUFFER, this.hlBuffer.subarray(0, hlIdx * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
842
1034
  gl.bindVertexArray(this.bgVAO);
843
- gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, hlCount);
844
- gl.disable(gl.BLEND);
1035
+ this.rebindBgInstanceAttribs(gl, this.overlayVBO);
1036
+ gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, hlIdx);
845
1037
  }
846
1038
  drawSelection() {
847
1039
  if (!this.gl || !this.grid || !this.selection)
@@ -854,12 +1046,12 @@ export class WebGLRenderer {
854
1046
  // Skip if selection is empty (same cell)
855
1047
  if (sr === er && sel.startCol === sel.endCol)
856
1048
  return;
857
- if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
1049
+ if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
858
1050
  return;
859
1051
  // Parse the selection background color
860
1052
  const selColor = hexToFloat4(this.theme.selectionBackground);
861
- // Build instance data for selection rects
862
- const selInstances = [];
1053
+ // Pack into pre-allocated selBuffer, growing only if needed
1054
+ let selIdx = 0;
863
1055
  for (let row = sr; row <= er; row++) {
864
1056
  let colStart;
865
1057
  let colEnd;
@@ -880,23 +1072,34 @@ export class WebGLRenderer {
880
1072
  colEnd = grid.cols - 1;
881
1073
  }
882
1074
  for (let col = colStart; col <= colEnd; col++) {
883
- selInstances.push(col, row, selColor[0], selColor[1], selColor[2], 0.5);
1075
+ const needed = (selIdx + 1) * BG_INSTANCE_FLOATS;
1076
+ if (needed > this.selBuffer.length) {
1077
+ const newBuf = new Float32Array(this.selBuffer.length * 2);
1078
+ newBuf.set(this.selBuffer);
1079
+ this.selBuffer = newBuf;
1080
+ }
1081
+ const off = selIdx * BG_INSTANCE_FLOATS;
1082
+ this.selBuffer[off] = col;
1083
+ this.selBuffer[off + 1] = row;
1084
+ this.selBuffer[off + 2] = selColor[0];
1085
+ this.selBuffer[off + 3] = selColor[1];
1086
+ this.selBuffer[off + 4] = selColor[2];
1087
+ this.selBuffer[off + 5] = 0.5;
1088
+ selIdx++;
884
1089
  }
885
1090
  }
886
- if (selInstances.length === 0)
1091
+ if (selIdx === 0)
887
1092
  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);
1093
+ // BLEND already enabled by caller
892
1094
  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);
1095
+ // Use cached uniform locations
1096
+ gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
1097
+ gl.uniform2f(this.bgCellSizeLoc, this.cellWidth * this.dpr, this.cellHeight * this.dpr);
1098
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
1099
+ gl.bufferData(gl.ARRAY_BUFFER, this.selBuffer.subarray(0, selIdx * BG_INSTANCE_FLOATS), gl.STREAM_DRAW);
897
1100
  gl.bindVertexArray(this.bgVAO);
898
- gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, selCount);
899
- gl.disable(gl.BLEND);
1101
+ this.rebindBgInstanceAttribs(gl, this.overlayVBO);
1102
+ gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, selIdx);
900
1103
  }
901
1104
  drawCursor() {
902
1105
  if (!this.gl || !this.cursor || !this.cursor.visible)
@@ -907,50 +1110,81 @@ export class WebGLRenderer {
907
1110
  const cellH = this.cellHeight * this.dpr;
908
1111
  const cc = this.themeCursorFloat;
909
1112
  // Use the bg program to draw a simple colored rect for the cursor
910
- if (!this.bgProgram || !this.bgVAO || !this.bgInstanceVBO)
1113
+ if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
911
1114
  return;
912
- gl.enable(gl.BLEND);
913
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1115
+ // BLEND already enabled by caller
914
1116
  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);
1117
+ // Use cached uniform locations
1118
+ gl.uniform2f(this.bgResolutionLoc, this.canvas?.width ?? 0, this.canvas?.height ?? 0);
1119
+ gl.uniform2f(this.bgCellSizeLoc, cellW, cellH);
1120
+ // Write into pre-allocated cursorData instead of allocating
917
1121
  // For bar and underline styles, we draw a thin rect.
918
1122
  // We abuse cellPos with fractional values to position correctly.
919
- let cursorData;
920
1123
  switch (cursor.style) {
921
1124
  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
- ]);
1125
+ this.cursorData[0] = cursor.col;
1126
+ this.cursorData[1] = cursor.row;
1127
+ this.cursorData[2] = cc[0];
1128
+ this.cursorData[3] = cc[1];
1129
+ this.cursorData[4] = cc[2];
1130
+ this.cursorData[5] = 0.5; // 50% alpha for block
930
1131
  break;
931
1132
  case "underline": {
932
1133
  // Draw a thin line at the bottom of the cell
933
- // We position it by adjusting cellPos row to be near bottom
934
1134
  const lineH = Math.max(2 * this.dpr, 1);
935
1135
  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
1136
+ this.cursorData[0] = cursor.col;
1137
+ this.cursorData[1] = fractionalRow;
1138
+ this.cursorData[2] = cc[0];
1139
+ this.cursorData[3] = cc[1];
1140
+ this.cursorData[4] = cc[2];
1141
+ this.cursorData[5] = cc[3];
939
1142
  break;
940
1143
  }
941
1144
  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]]);
1145
+ this.cursorData[0] = cursor.col;
1146
+ this.cursorData[1] = cursor.row;
1147
+ this.cursorData[2] = cc[0];
1148
+ this.cursorData[3] = cc[1];
1149
+ this.cursorData[4] = cc[2];
1150
+ this.cursorData[5] = cc[3];
944
1151
  break;
945
1152
  }
946
1153
  default:
947
- cursorData = new Float32Array([cursor.col, cursor.row, cc[0], cc[1], cc[2], 0.5]);
1154
+ this.cursorData[0] = cursor.col;
1155
+ this.cursorData[1] = cursor.row;
1156
+ this.cursorData[2] = cc[0];
1157
+ this.cursorData[3] = cc[1];
1158
+ this.cursorData[4] = cc[2];
1159
+ this.cursorData[5] = 0.5;
948
1160
  }
949
- gl.bindBuffer(gl.ARRAY_BUFFER, this.bgInstanceVBO);
950
- gl.bufferData(gl.ARRAY_BUFFER, cursorData, gl.DYNAMIC_DRAW);
1161
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.overlayVBO);
1162
+ gl.bufferData(gl.ARRAY_BUFFER, this.cursorData, gl.STREAM_DRAW);
951
1163
  gl.bindVertexArray(this.bgVAO);
1164
+ this.rebindBgInstanceAttribs(gl, this.overlayVBO);
952
1165
  gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, 1);
953
- gl.disable(gl.BLEND);
1166
+ }
1167
+ // -----------------------------------------------------------------------
1168
+ // Instance attribute rebinding helpers for double-buffered VBOs
1169
+ // -----------------------------------------------------------------------
1170
+ // Cached attribute locations (populated in initGLResources)
1171
+ bgAttribLocs = { cellPos: -1, color: -1 };
1172
+ glyphAttribLocs = { cellPos: -1, color: -1, texCoord: -1, glyphSize: -1 };
1173
+ rebindBgInstanceAttribs(gl, vbo) {
1174
+ const FLOAT = 4;
1175
+ const stride = BG_INSTANCE_FLOATS * FLOAT;
1176
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
1177
+ gl.vertexAttribPointer(this.bgAttribLocs.cellPos, 2, gl.FLOAT, false, stride, 0);
1178
+ gl.vertexAttribPointer(this.bgAttribLocs.color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
1179
+ }
1180
+ rebindGlyphInstanceAttribs(gl, vbo) {
1181
+ const FLOAT = 4;
1182
+ const stride = GLYPH_INSTANCE_FLOATS * FLOAT;
1183
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
1184
+ gl.vertexAttribPointer(this.glyphAttribLocs.cellPos, 2, gl.FLOAT, false, stride, 0);
1185
+ gl.vertexAttribPointer(this.glyphAttribLocs.color, 4, gl.FLOAT, false, stride, 2 * FLOAT);
1186
+ gl.vertexAttribPointer(this.glyphAttribLocs.texCoord, 4, gl.FLOAT, false, stride, 6 * FLOAT);
1187
+ gl.vertexAttribPointer(this.glyphAttribLocs.glyphSize, 2, gl.FLOAT, false, stride, 10 * FLOAT);
954
1188
  }
955
1189
  // -----------------------------------------------------------------------
956
1190
  // Internal helpers
@@ -1008,8 +1242,7 @@ export class WebGLRenderer {
1008
1242
  let font = "";
1009
1243
  if (italic)
1010
1244
  font += "italic ";
1011
- if (bold)
1012
- font += "bold ";
1245
+ font += `${bold ? this.fontWeightBold : this.fontWeight} `;
1013
1246
  font += `${this.fontSize}px ${this.fontFamily}`;
1014
1247
  return font;
1015
1248
  }