@next_term/web 0.1.0-next.0 → 0.1.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/accessibility.d.ts +2 -4
- package/dist/accessibility.d.ts.map +1 -1
- package/dist/accessibility.js +2 -7
- package/dist/accessibility.js.map +1 -1
- package/dist/addon.d.ts +9 -0
- package/dist/addons/fit.d.ts +23 -0
- package/dist/addons/web-links.d.ts +0 -1
- package/dist/addons/web-links.d.ts.map +1 -1
- package/dist/addons/web-links.js +0 -4
- package/dist/addons/web-links.js.map +1 -1
- package/dist/fit.d.ts +9 -0
- package/dist/fit.d.ts.map +1 -1
- package/dist/fit.js +9 -1
- package/dist/fit.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/input-handler.d.ts +7 -0
- package/dist/input-handler.d.ts.map +1 -1
- package/dist/input-handler.js +36 -5
- package/dist/input-handler.js.map +1 -1
- package/dist/parser-worker.d.ts +34 -0
- package/dist/parser-worker.d.ts.map +1 -1
- package/dist/parser-worker.js +4 -17
- package/dist/parser-worker.js.map +1 -1
- package/dist/render-bridge.d.ts +3 -1
- package/dist/render-bridge.d.ts.map +1 -1
- package/dist/render-bridge.js +5 -1
- package/dist/render-bridge.js.map +1 -1
- package/dist/render-worker.d.ts +4 -0
- package/dist/render-worker.d.ts.map +1 -1
- package/dist/render-worker.js +211 -98
- package/dist/render-worker.js.map +1 -1
- package/dist/renderer.d.ts +7 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +21 -6
- package/dist/renderer.js.map +1 -1
- package/dist/shared-context.d.ts +38 -9
- package/dist/shared-context.d.ts.map +1 -1
- package/dist/shared-context.js +491 -193
- package/dist/shared-context.js.map +1 -1
- package/dist/web-terminal.d.ts +26 -1
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/web-terminal.js +140 -18
- package/dist/web-terminal.js.map +1 -1
- package/dist/webgl-renderer.d.ts +33 -7
- package/dist/webgl-renderer.d.ts.map +1 -1
- package/dist/webgl-renderer.js +372 -139
- package/dist/webgl-renderer.js.map +1 -1
- package/dist/webgl-utils.d.ts +4 -0
- package/dist/webgl-utils.d.ts.map +1 -0
- package/dist/webgl-utils.js +19 -0
- package/dist/webgl-utils.js.map +1 -0
- package/dist/worker-bridge.d.ts +7 -2
- package/dist/worker-bridge.d.ts.map +1 -1
- package/dist/worker-bridge.js +41 -13
- package/dist/worker-bridge.js.map +1 -1
- package/package.json +6 -6
package/dist/webgl-renderer.js
CHANGED
|
@@ -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
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
//
|
|
486
|
-
this.
|
|
487
|
-
|
|
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
|
|
497
|
-
|
|
498
|
-
|
|
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 —
|
|
506
|
-
packBgInstance(this.bgInstances,
|
|
507
|
-
|
|
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,
|
|
517
|
-
|
|
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 &&
|
|
688
|
+
if (this.bgCount > 0 && this.bgProgram && this.bgVAO && activeBgVBO) {
|
|
535
689
|
gl.useProgram(this.bgProgram);
|
|
536
|
-
|
|
537
|
-
gl.uniform2f(
|
|
538
|
-
gl.
|
|
539
|
-
|
|
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 &&
|
|
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
|
-
|
|
549
|
-
gl.uniform2f(
|
|
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(
|
|
554
|
-
|
|
555
|
-
gl.
|
|
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.
|
|
635
|
-
gl.deleteBuffer(this.
|
|
636
|
-
if (this.
|
|
637
|
-
gl.deleteBuffer(this.
|
|
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
|
-
//
|
|
697
|
-
this.
|
|
698
|
-
this.
|
|
699
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
999
|
+
if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
|
|
819
1000
|
return;
|
|
820
|
-
|
|
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
|
-
|
|
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 (
|
|
1025
|
+
if (hlIdx === 0)
|
|
832
1026
|
return;
|
|
833
|
-
|
|
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
|
-
|
|
839
|
-
gl.uniform2f(
|
|
840
|
-
gl.
|
|
841
|
-
gl.
|
|
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
|
-
|
|
844
|
-
gl.
|
|
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.
|
|
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
|
-
//
|
|
862
|
-
|
|
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
|
-
|
|
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 (
|
|
1091
|
+
if (selIdx === 0)
|
|
887
1092
|
return;
|
|
888
|
-
|
|
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
|
-
|
|
894
|
-
gl.uniform2f(
|
|
895
|
-
gl.
|
|
896
|
-
gl.
|
|
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
|
-
|
|
899
|
-
gl.
|
|
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.
|
|
1113
|
+
if (!this.bgProgram || !this.bgVAO || !this.overlayVBO)
|
|
911
1114
|
return;
|
|
912
|
-
|
|
913
|
-
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
1115
|
+
// BLEND already enabled by caller
|
|
914
1116
|
gl.useProgram(this.bgProgram);
|
|
915
|
-
|
|
916
|
-
gl.uniform2f(
|
|
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 =
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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 =
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
943
|
-
cursorData =
|
|
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 =
|
|
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.
|
|
950
|
-
gl.bufferData(gl.ARRAY_BUFFER, cursorData, gl.
|
|
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
|
-
|
|
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
|
-
|
|
1012
|
-
font += "bold ";
|
|
1245
|
+
font += `${bold ? this.fontWeightBold : this.fontWeight} `;
|
|
1013
1246
|
font += `${this.fontSize}px ${this.fontFamily}`;
|
|
1014
1247
|
return font;
|
|
1015
1248
|
}
|