@aurigax-ai/ghostty-web 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/addons/search.ts +190 -0
- package/lib/ghostty.ts +224 -6
- package/lib/glyph-ink.ts +42 -0
- package/lib/index.ts +6 -0
- package/lib/input-handler.ts +42 -1
- package/lib/interfaces.ts +23 -0
- package/lib/providers/osc8-link-provider.ts +4 -10
- package/lib/providers/url-regex-provider.ts +10 -13
- package/lib/providers/web-link.ts +35 -0
- package/lib/renderer.ts +266 -33
- package/lib/terminal.ts +150 -58
- package/lib/types.ts +2 -1
- package/lib/webgl-instances.ts +69 -0
- package/lib/webgl-renderer.ts +176 -103
- package/package.json +1 -1
package/lib/terminal.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { getGhostty } from './index';
|
|
31
31
|
import { InputHandler, type MouseTrackingConfig } from './input-handler';
|
|
32
32
|
import type {
|
|
33
|
+
FontWeight,
|
|
33
34
|
IBufferNamespace,
|
|
34
35
|
IBufferRange,
|
|
35
36
|
IDisposable,
|
|
@@ -44,7 +45,13 @@ import { LinkDetector } from './link-detector';
|
|
|
44
45
|
import { type IMarker, Marker } from './marker';
|
|
45
46
|
import { OSC8LinkProvider } from './providers/osc8-link-provider';
|
|
46
47
|
import { UrlRegexProvider } from './providers/url-regex-provider';
|
|
47
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
CanvasRenderer,
|
|
50
|
+
type RendererOptions,
|
|
51
|
+
type SearchHighlight,
|
|
52
|
+
type SearchHighlightColors,
|
|
53
|
+
type TerminalRenderer,
|
|
54
|
+
} from './renderer';
|
|
48
55
|
import { SelectionManager } from './selection-manager';
|
|
49
56
|
import type { ILink, ILinkProvider } from './types';
|
|
50
57
|
import { WebglRenderer } from './webgl-renderer';
|
|
@@ -97,6 +104,7 @@ export class Terminal implements ITerminalCore {
|
|
|
97
104
|
private scrollEmitter = new EventEmitter<number>();
|
|
98
105
|
private renderEmitter = new EventEmitter<{ start: number; end: number }>();
|
|
99
106
|
private cursorMoveEmitter = new EventEmitter<void>();
|
|
107
|
+
private writeParsedEmitter = new EventEmitter<void>();
|
|
100
108
|
// Public event accessors (xterm.js compatibility)
|
|
101
109
|
public readonly onData: IEvent<string> = this.dataEmitter.event;
|
|
102
110
|
public readonly onResize: IEvent<{ cols: number; rows: number }> = this.resizeEmitter.event;
|
|
@@ -107,6 +115,8 @@ export class Terminal implements ITerminalCore {
|
|
|
107
115
|
public readonly onScroll: IEvent<number> = this.scrollEmitter.event;
|
|
108
116
|
public readonly onRender: IEvent<{ start: number; end: number }> = this.renderEmitter.event;
|
|
109
117
|
public readonly onCursorMove: IEvent<void> = this.cursorMoveEmitter.event;
|
|
118
|
+
/** Fires after written data was parsed, as xterm.js's onWriteParsed. */
|
|
119
|
+
public readonly onWriteParsed: IEvent<void> = this.writeParsedEmitter.event;
|
|
110
120
|
|
|
111
121
|
private pwdEmitter = new EventEmitter<string>();
|
|
112
122
|
private semanticPromptEmitter = new EventEmitter<SemanticPromptEvent>();
|
|
@@ -188,10 +198,17 @@ export class Terminal implements ITerminalCore {
|
|
|
188
198
|
fontSize: options.fontSize ?? 15,
|
|
189
199
|
fontFamily: options.fontFamily ?? 'monospace',
|
|
190
200
|
allowTransparency: options.allowTransparency ?? false,
|
|
201
|
+
linkHandler: options.linkHandler ?? null,
|
|
191
202
|
renderer: options.renderer ?? 'webgl',
|
|
192
203
|
convertEol: options.convertEol ?? false,
|
|
193
204
|
disableStdin: options.disableStdin ?? false,
|
|
194
205
|
smoothScrollDuration: options.smoothScrollDuration ?? 100, // Default: 100ms smooth scroll
|
|
206
|
+
fontWeight: options.fontWeight ?? 'normal',
|
|
207
|
+
fontWeightBold: options.fontWeightBold ?? 'bold',
|
|
208
|
+
lineHeight: options.lineHeight ?? 1,
|
|
209
|
+
scrollSensitivity: options.scrollSensitivity ?? 1,
|
|
210
|
+
minimumContrastRatio: options.minimumContrastRatio ?? 1,
|
|
211
|
+
macOptionIsMeta: options.macOptionIsMeta ?? false,
|
|
195
212
|
};
|
|
196
213
|
|
|
197
214
|
// Wrap in Proxy to intercept runtime changes (xterm.js compatibility)
|
|
@@ -243,11 +260,37 @@ export class Terminal implements ITerminalCore {
|
|
|
243
260
|
break;
|
|
244
261
|
|
|
245
262
|
case 'theme':
|
|
263
|
+
this.wasmTerm?.configure(this.themeConfig());
|
|
264
|
+
if (this.renderer && this.wasmTerm) {
|
|
265
|
+
this.renderer.setTheme(this.options.theme);
|
|
266
|
+
this.renderer.render(this.wasmTerm, true, this.viewportY, this, this.scrollbarOpacity);
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
case 'macOptionIsMeta':
|
|
271
|
+
if (this.inputHandler) this.inputHandler.macOptionIsMeta = this.options.macOptionIsMeta;
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case 'fontWeight':
|
|
275
|
+
case 'fontWeightBold':
|
|
276
|
+
case 'lineHeight':
|
|
246
277
|
if (this.renderer) {
|
|
247
|
-
|
|
278
|
+
this.renderer.setFontOptions(this.fontOptions());
|
|
279
|
+
this.handleFontChange();
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
|
|
283
|
+
case 'minimumContrastRatio':
|
|
284
|
+
if (this.renderer && this.wasmTerm) {
|
|
285
|
+
this.renderer.setMinimumContrastRatio(this.options.minimumContrastRatio);
|
|
286
|
+
this.renderer.render(this.wasmTerm, true, this.viewportY, this, this.scrollbarOpacity);
|
|
248
287
|
}
|
|
249
288
|
break;
|
|
250
289
|
|
|
290
|
+
case 'scrollback':
|
|
291
|
+
this.wasmTerm?.configure({ scrollbackLimit: this.options.scrollback });
|
|
292
|
+
break;
|
|
293
|
+
|
|
251
294
|
case 'fontSize':
|
|
252
295
|
if (this.renderer) {
|
|
253
296
|
this.renderer.setFontSize(this.options.fontSize);
|
|
@@ -293,67 +336,48 @@ export class Terminal implements ITerminalCore {
|
|
|
293
336
|
* Parse a CSS color string to 0xRRGGBB format.
|
|
294
337
|
* Returns 0 if the color is undefined or invalid.
|
|
295
338
|
*/
|
|
296
|
-
private parseColorToHex(color?: string): number {
|
|
297
|
-
if (!color) return
|
|
298
|
-
|
|
299
|
-
// Handle hex colors (#RGB, #RRGGBB)
|
|
339
|
+
private parseColorToHex(color?: string): number | undefined {
|
|
340
|
+
if (!color) return undefined;
|
|
300
341
|
if (color.startsWith('#')) {
|
|
301
342
|
let hex = color.slice(1);
|
|
302
|
-
if (hex.length === 3) {
|
|
343
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
303
344
|
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
304
345
|
}
|
|
305
|
-
const value = Number.parseInt(hex, 16);
|
|
306
|
-
return Number.isNaN(value) ?
|
|
346
|
+
const value = Number.parseInt(hex.slice(0, 6), 16);
|
|
347
|
+
return hex.length >= 6 && !Number.isNaN(value) ? value : undefined;
|
|
307
348
|
}
|
|
308
|
-
|
|
309
|
-
// Handle rgb(r, g, b) format
|
|
310
|
-
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
349
|
+
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
311
350
|
if (match) {
|
|
312
351
|
const r = Number.parseInt(match[1], 10);
|
|
313
352
|
const g = Number.parseInt(match[2], 10);
|
|
314
353
|
const b = Number.parseInt(match[3], 10);
|
|
315
354
|
return (r << 16) | (g << 8) | b;
|
|
316
355
|
}
|
|
317
|
-
|
|
318
|
-
return 0;
|
|
356
|
+
return undefined;
|
|
319
357
|
}
|
|
320
358
|
|
|
321
|
-
/**
|
|
322
|
-
|
|
323
|
-
*/
|
|
324
|
-
private buildWasmConfig(): GhosttyTerminalConfig | undefined {
|
|
359
|
+
/** Theme colors in the form libghostty-vt takes; a color the theme leaves out keeps Ghostty's default. */
|
|
360
|
+
private themeConfig(): GhosttyTerminalConfig {
|
|
325
361
|
const theme = this.options.theme;
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
this.parseColorToHex(theme?.white),
|
|
345
|
-
this.parseColorToHex(theme?.brightBlack),
|
|
346
|
-
this.parseColorToHex(theme?.brightRed),
|
|
347
|
-
this.parseColorToHex(theme?.brightGreen),
|
|
348
|
-
this.parseColorToHex(theme?.brightYellow),
|
|
349
|
-
this.parseColorToHex(theme?.brightBlue),
|
|
350
|
-
this.parseColorToHex(theme?.brightMagenta),
|
|
351
|
-
this.parseColorToHex(theme?.brightCyan),
|
|
352
|
-
this.parseColorToHex(theme?.brightWhite),
|
|
353
|
-
];
|
|
354
|
-
|
|
362
|
+
const palette = [
|
|
363
|
+
theme?.black,
|
|
364
|
+
theme?.red,
|
|
365
|
+
theme?.green,
|
|
366
|
+
theme?.yellow,
|
|
367
|
+
theme?.blue,
|
|
368
|
+
theme?.magenta,
|
|
369
|
+
theme?.cyan,
|
|
370
|
+
theme?.white,
|
|
371
|
+
theme?.brightBlack,
|
|
372
|
+
theme?.brightRed,
|
|
373
|
+
theme?.brightGreen,
|
|
374
|
+
theme?.brightYellow,
|
|
375
|
+
theme?.brightBlue,
|
|
376
|
+
theme?.brightMagenta,
|
|
377
|
+
theme?.brightCyan,
|
|
378
|
+
theme?.brightWhite,
|
|
379
|
+
].map((color) => this.parseColorToHex(color));
|
|
355
380
|
return {
|
|
356
|
-
scrollbackLimit: scrollback,
|
|
357
381
|
fgColor: this.parseColorToHex(theme?.foreground),
|
|
358
382
|
bgColor: this.parseColorToHex(theme?.background),
|
|
359
383
|
cursorColor: this.parseColorToHex(theme?.cursor),
|
|
@@ -361,6 +385,10 @@ export class Terminal implements ITerminalCore {
|
|
|
361
385
|
};
|
|
362
386
|
}
|
|
363
387
|
|
|
388
|
+
private buildWasmConfig(): GhosttyTerminalConfig {
|
|
389
|
+
return { ...this.themeConfig(), scrollbackLimit: this.options.scrollback };
|
|
390
|
+
}
|
|
391
|
+
|
|
364
392
|
// ==========================================================================
|
|
365
393
|
// Lifecycle Methods
|
|
366
394
|
// ==========================================================================
|
|
@@ -506,6 +534,7 @@ export class Terminal implements ITerminalCore {
|
|
|
506
534
|
this.textarea,
|
|
507
535
|
mouseConfig
|
|
508
536
|
);
|
|
537
|
+
this.inputHandler.macOptionIsMeta = this.options.macOptionIsMeta;
|
|
509
538
|
|
|
510
539
|
// Create selection manager (pass textarea for context menu positioning)
|
|
511
540
|
this.selectionManager = new SelectionManager(
|
|
@@ -537,6 +566,8 @@ export class Terminal implements ITerminalCore {
|
|
|
537
566
|
parent.addEventListener('mousedown', this.handleMouseDown, { capture: true });
|
|
538
567
|
parent.addEventListener('mousemove', this.handleMouseMove);
|
|
539
568
|
parent.addEventListener('mouseleave', this.handleMouseLeave);
|
|
569
|
+
parent.addEventListener('focusin', this.handleFocusIn);
|
|
570
|
+
parent.addEventListener('focusout', this.handleFocusOut);
|
|
540
571
|
parent.addEventListener('click', this.handleClick);
|
|
541
572
|
|
|
542
573
|
// Setup document-level mouseup for scrollbar drag (so drag works even outside canvas)
|
|
@@ -567,8 +598,22 @@ export class Terminal implements ITerminalCore {
|
|
|
567
598
|
return this.renderer instanceof WebglRenderer ? 'webgl' : 'canvas';
|
|
568
599
|
}
|
|
569
600
|
|
|
601
|
+
private fontOptions(): {
|
|
602
|
+
fontWeight: FontWeight;
|
|
603
|
+
fontWeightBold: FontWeight;
|
|
604
|
+
lineHeight: number;
|
|
605
|
+
} {
|
|
606
|
+
return {
|
|
607
|
+
fontWeight: this.options.fontWeight,
|
|
608
|
+
fontWeightBold: this.options.fontWeightBold,
|
|
609
|
+
lineHeight: this.options.lineHeight,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
570
613
|
private rendererOptions(): RendererOptions {
|
|
571
614
|
return {
|
|
615
|
+
...this.fontOptions(),
|
|
616
|
+
minimumContrastRatio: this.options.minimumContrastRatio,
|
|
572
617
|
fontSize: this.options.fontSize,
|
|
573
618
|
fontFamily: this.options.fontFamily,
|
|
574
619
|
cursorStyle: this.options.cursorStyle,
|
|
@@ -589,7 +634,8 @@ export class Terminal implements ITerminalCore {
|
|
|
589
634
|
}
|
|
590
635
|
}
|
|
591
636
|
renderer ??= new CanvasRenderer(this.canvas!, this.rendererOptions());
|
|
592
|
-
renderer.onNeedsFrame = () => this.
|
|
637
|
+
renderer.onNeedsFrame = () => this.requestFrame();
|
|
638
|
+
renderer.setFocused(this.focused);
|
|
593
639
|
return renderer;
|
|
594
640
|
}
|
|
595
641
|
|
|
@@ -599,8 +645,10 @@ export class Terminal implements ITerminalCore {
|
|
|
599
645
|
const hoveredLink = this.renderer.getHoveredHyperlinkId();
|
|
600
646
|
this.renderer.dispose();
|
|
601
647
|
const renderer = new CanvasRenderer(this.canvas, this.rendererOptions());
|
|
602
|
-
renderer.onNeedsFrame = () => this.
|
|
648
|
+
renderer.onNeedsFrame = () => this.requestFrame();
|
|
603
649
|
renderer.setHoveredHyperlinkId(hoveredLink);
|
|
650
|
+
renderer.setFocused(this.focused);
|
|
651
|
+
renderer.setSearchHighlights(this.searchHighlights.list, this.searchHighlights.colors);
|
|
604
652
|
if (this.selectionManager) {
|
|
605
653
|
renderer.setSelectionManager(this.selectionManager);
|
|
606
654
|
this.selectionManager.setRenderer(renderer);
|
|
@@ -1035,16 +1083,18 @@ export class Terminal implements ITerminalCore {
|
|
|
1035
1083
|
}
|
|
1036
1084
|
}
|
|
1037
1085
|
|
|
1038
|
-
/**
|
|
1039
|
-
* Scroll viewport to a specific line in the buffer
|
|
1040
|
-
* @param line Line number (0 = top of scrollback, scrollbackLength = bottom)
|
|
1041
|
-
*/
|
|
1042
1086
|
/** Scrolls so that absolute buffer line `line` (0 = oldest scrollback line) is at the top, like xterm.js. */
|
|
1043
1087
|
public scrollToLine(line: number): void {
|
|
1044
|
-
this.wake();
|
|
1045
1088
|
const scrollbackLength = this.getScrollbackLength();
|
|
1046
1089
|
const top = Math.max(0, Math.min(scrollbackLength, Math.round(line)));
|
|
1047
|
-
|
|
1090
|
+
this.scrollToViewportY(scrollbackLength - top);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Scrolls to `viewportY` lines above the bottom. */
|
|
1094
|
+
private scrollToViewportY(viewportY: number): void {
|
|
1095
|
+
this.wake();
|
|
1096
|
+
const scrollbackLength = this.getScrollbackLength();
|
|
1097
|
+
const newViewportY = Math.max(0, Math.min(scrollbackLength, Math.round(viewportY)));
|
|
1048
1098
|
|
|
1049
1099
|
if (newViewportY !== this.viewportY) {
|
|
1050
1100
|
this.viewportY = newViewportY;
|
|
@@ -1201,6 +1251,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1201
1251
|
this.scrollEmitter.dispose();
|
|
1202
1252
|
this.renderEmitter.dispose();
|
|
1203
1253
|
this.cursorMoveEmitter.dispose();
|
|
1254
|
+
this.writeParsedEmitter.dispose();
|
|
1204
1255
|
}
|
|
1205
1256
|
|
|
1206
1257
|
// ==========================================================================
|
|
@@ -1308,6 +1359,8 @@ export class Terminal implements ITerminalCore {
|
|
|
1308
1359
|
this.element.removeEventListener('mousedown', this.handleMouseDown, { capture: true });
|
|
1309
1360
|
this.element.removeEventListener('mousemove', this.handleMouseMove);
|
|
1310
1361
|
this.element.removeEventListener('mouseleave', this.handleMouseLeave);
|
|
1362
|
+
this.element.removeEventListener('focusin', this.handleFocusIn);
|
|
1363
|
+
this.element.removeEventListener('focusout', this.handleFocusOut);
|
|
1311
1364
|
this.element.removeEventListener('click', this.handleClick);
|
|
1312
1365
|
|
|
1313
1366
|
// Remove contenteditable and accessibility attributes added in open()
|
|
@@ -1663,6 +1716,8 @@ export class Terminal implements ITerminalCore {
|
|
|
1663
1716
|
deltaLines = e.deltaY / 33;
|
|
1664
1717
|
}
|
|
1665
1718
|
|
|
1719
|
+
deltaLines *= this.options.scrollSensitivity;
|
|
1720
|
+
|
|
1666
1721
|
// Use smooth scrolling for any amount (no rounding needed)
|
|
1667
1722
|
if (deltaLines !== 0) {
|
|
1668
1723
|
// Calculate target position
|
|
@@ -1728,7 +1783,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1728
1783
|
const relativeY = mouseY - scrollbarPadding;
|
|
1729
1784
|
const scrollFraction = 1 - relativeY / scrollbarTrackHeight; // Inverted: top = 1, bottom = 0
|
|
1730
1785
|
const targetViewportY = Math.round(scrollFraction * scrollbackLength);
|
|
1731
|
-
this.
|
|
1786
|
+
this.scrollToViewportY(targetViewportY);
|
|
1732
1787
|
}
|
|
1733
1788
|
}
|
|
1734
1789
|
};
|
|
@@ -1786,7 +1841,7 @@ export class Terminal implements ITerminalCore {
|
|
|
1786
1841
|
const viewportDelta = Math.round(scrollFraction * scrollbackLength);
|
|
1787
1842
|
|
|
1788
1843
|
const newViewportY = this.scrollbarDragStartViewportY + viewportDelta;
|
|
1789
|
-
this.
|
|
1844
|
+
this.scrollToViewportY(newViewportY);
|
|
1790
1845
|
}
|
|
1791
1846
|
|
|
1792
1847
|
/**
|
|
@@ -1965,6 +2020,30 @@ export class Terminal implements ITerminalCore {
|
|
|
1965
2020
|
};
|
|
1966
2021
|
}
|
|
1967
2022
|
|
|
2023
|
+
private focused = false;
|
|
2024
|
+
|
|
2025
|
+
private readonly handleFocusIn = (): void => this.setFocused(true);
|
|
2026
|
+
|
|
2027
|
+
private readonly handleFocusOut = (event: FocusEvent): void => {
|
|
2028
|
+
const next = event.relatedTarget;
|
|
2029
|
+
if (next instanceof Node && this.element?.contains(next)) return;
|
|
2030
|
+
this.setFocused(false);
|
|
2031
|
+
};
|
|
2032
|
+
|
|
2033
|
+
private setFocused(focused: boolean): void {
|
|
2034
|
+
if (focused === this.focused) return;
|
|
2035
|
+
this.focused = focused;
|
|
2036
|
+
this.renderer?.setFocused(focused);
|
|
2037
|
+
this.requestFrame();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/** Draws one frame, for a change that needs no follow-up frames (a cursor blink). */
|
|
2041
|
+
private requestFrame(): void {
|
|
2042
|
+
if (this.animationFrameId) return;
|
|
2043
|
+
this.idleFrames = Terminal.IDLE_FRAMES_BEFORE_SLEEP;
|
|
2044
|
+
if (this.isOpen && !this.isDisposed) this.startRenderLoop();
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1968
2047
|
/** Keeps the render loop running for a while; called whenever something may need drawing. */
|
|
1969
2048
|
public wake(): void {
|
|
1970
2049
|
this.idleFrames = 0;
|
|
@@ -1999,6 +2078,19 @@ export class Terminal implements ITerminalCore {
|
|
|
1999
2078
|
(this.buffer as BufferNamespace)._fireBufferChange(this.buffer.active);
|
|
2000
2079
|
}
|
|
2001
2080
|
for (const marker of [...this.markers]) marker.refresh();
|
|
2081
|
+
this.writeParsedEmitter.fire();
|
|
2082
|
+
this.wake();
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
private searchHighlights: {
|
|
2086
|
+
list: SearchHighlight[] | null;
|
|
2087
|
+
colors?: SearchHighlightColors;
|
|
2088
|
+
} = { list: null };
|
|
2089
|
+
|
|
2090
|
+
/** Shows search matches on viewport rows (null clears them). Used by SearchAddon. */
|
|
2091
|
+
setSearchHighlights(list: SearchHighlight[] | null, colors?: SearchHighlightColors): void {
|
|
2092
|
+
this.searchHighlights = { list, colors: colors ?? this.searchHighlights.colors };
|
|
2093
|
+
this.renderer?.setSearchHighlights(list, colors);
|
|
2002
2094
|
this.wake();
|
|
2003
2095
|
}
|
|
2004
2096
|
|
package/lib/types.ts
CHANGED
|
@@ -394,7 +394,8 @@ export interface GhosttyTerminalConfig {
|
|
|
394
394
|
fgColor?: number;
|
|
395
395
|
bgColor?: number;
|
|
396
396
|
cursorColor?: number;
|
|
397
|
-
|
|
397
|
+
/** The 16 ANSI colors; an undefined entry keeps Ghostty's default. */
|
|
398
|
+
palette?: (number | undefined)[];
|
|
398
399
|
}
|
|
399
400
|
|
|
400
401
|
/**
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packs the renderer's per-cell instance slots into dense arrays so a frame
|
|
3
|
+
* draws only the instances that show something. Slots with a zero width are
|
|
4
|
+
* empty. Rects that continue the previous one on the same row with the same
|
|
5
|
+
* color are merged into it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const RECT_WORDS = 5;
|
|
9
|
+
export const GLYPH_WORDS = 8;
|
|
10
|
+
|
|
11
|
+
export interface InstanceArrays {
|
|
12
|
+
words: Float32Array;
|
|
13
|
+
bits: Uint32Array;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function instanceArrays(instances: number, wordsPer: number): InstanceArrays {
|
|
17
|
+
const buffer = new ArrayBuffer(instances * wordsPer * 4);
|
|
18
|
+
return { words: new Float32Array(buffer), bits: new Uint32Array(buffer) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function compactRects(
|
|
22
|
+
src: InstanceArrays,
|
|
23
|
+
first: number,
|
|
24
|
+
count: number,
|
|
25
|
+
out: InstanceArrays
|
|
26
|
+
): number {
|
|
27
|
+
const { words, bits } = src;
|
|
28
|
+
const outWords = out.words;
|
|
29
|
+
const outBits = out.bits;
|
|
30
|
+
let n = 0;
|
|
31
|
+
let last = -1;
|
|
32
|
+
for (let i = first; i < first + count; i++) {
|
|
33
|
+
const at = i * RECT_WORDS;
|
|
34
|
+
const width = words[at + 2];
|
|
35
|
+
if (width === 0) continue;
|
|
36
|
+
if (
|
|
37
|
+
last >= 0 &&
|
|
38
|
+
bits[at + 4] === outBits[last + 4] &&
|
|
39
|
+
words[at + 1] === outWords[last + 1] &&
|
|
40
|
+
words[at + 3] === outWords[last + 3] &&
|
|
41
|
+
outWords[last] + outWords[last + 2] === words[at]
|
|
42
|
+
) {
|
|
43
|
+
outWords[last + 2] += width;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
last = n * RECT_WORDS;
|
|
47
|
+
outBits[last] = bits[at];
|
|
48
|
+
outBits[last + 1] = bits[at + 1];
|
|
49
|
+
outBits[last + 2] = bits[at + 2];
|
|
50
|
+
outBits[last + 3] = bits[at + 3];
|
|
51
|
+
outBits[last + 4] = bits[at + 4];
|
|
52
|
+
n++;
|
|
53
|
+
}
|
|
54
|
+
return n;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function compactGlyphs(src: InstanceArrays, count: number, out: InstanceArrays): number {
|
|
58
|
+
const { words, bits } = src;
|
|
59
|
+
const outBits = out.bits;
|
|
60
|
+
let n = 0;
|
|
61
|
+
for (let i = 0; i < count; i++) {
|
|
62
|
+
const at = i * GLYPH_WORDS;
|
|
63
|
+
if (words[at + 4] === 0) continue;
|
|
64
|
+
const to = n * GLYPH_WORDS;
|
|
65
|
+
for (let w = 0; w < GLYPH_WORDS; w++) outBits[to + w] = bits[at + w];
|
|
66
|
+
n++;
|
|
67
|
+
}
|
|
68
|
+
return n;
|
|
69
|
+
}
|