mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #3926 from Tyriar/rounding
FINALLY fix blurry canvas/webgl renderer issues due to device pixel rounding errors 🎉
This commit is contained in:
@@ -49,6 +49,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
italic: false
|
||||
};
|
||||
|
||||
public get canvas(): HTMLCanvasElement { return this._canvas; }
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services';
|
||||
import { removeTerminalFromCache } from './atlas/CharAtlasCache';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
|
||||
|
||||
let nextRendererId = 1;
|
||||
|
||||
@@ -62,6 +63,9 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
};
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this._updateDimensions();
|
||||
|
||||
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
||||
|
||||
this.onOptionsChanged();
|
||||
}
|
||||
|
||||
@@ -167,53 +171,32 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be
|
||||
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
|
||||
// blurry. When text is drawn to the grid not using the CharAtlas, it is
|
||||
// clipped to ensure there is no overlap with the next cell.
|
||||
// See the WebGL renderer for an explanation of this section.
|
||||
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
|
||||
|
||||
// Calculate the scaled character height. Height is ceiled in case
|
||||
// devicePixelRatio is a floating point number in order to ensure there is
|
||||
// enough space to draw the character to the cell.
|
||||
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
|
||||
|
||||
// Calculate the scaled cell height, if lineHeight is not 1 then the value
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
|
||||
|
||||
// The the size of the canvas on the page. It's very important that this
|
||||
// rounds to nearest integer and not ceils as browsers often set
|
||||
// window.devicePixelRatio as something like 1.100000023841858, when it's
|
||||
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
|
||||
// pixel too large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
|
||||
|
||||
// Get the _actual_ dimensions of an individual cell. This needs to be
|
||||
// derived from the canvasWidth/Height calculated above which takes into
|
||||
// account window.devicePixelRatio. ICharSizeService.width/height by itself
|
||||
// is insufficient when the page is not at 100% zoom level as it's measured
|
||||
// in CSS pixels, but the actual char size on the canvas can differ.
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
|
||||
}
|
||||
|
||||
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
||||
this.dimensions.scaledCanvasHeight = height;
|
||||
this.dimensions.scaledCanvasWidth = width;
|
||||
// Resize all render layers
|
||||
for (const l of this._renderLayers) {
|
||||
l.resize(this.dimensions);
|
||||
}
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
private _requestRedrawViewport(): void {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -57,6 +57,8 @@ export interface IRenderer extends IDisposable {
|
||||
}
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
|
||||
@@ -11,11 +11,12 @@ import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IEvent } from 'xterm';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
|
||||
import { ITerminal, IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
@@ -95,6 +96,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
|
||||
this.register(observeDevicePixelDimensions(this._canvas, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
||||
|
||||
this._core.screenElement!.appendChild(this._canvas);
|
||||
|
||||
@@ -517,67 +519,63 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be
|
||||
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
|
||||
// blurry. When text is drawn to the grid not using the CharAtlas, it is
|
||||
// clipped to ensure there is no overlap with the next cell.
|
||||
|
||||
// NOTE: ceil fixes sometime, floor does others :s
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be drawn to an integer grid
|
||||
// in order for the char atlas glyphs to not be blurry.
|
||||
this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled character height. Height is ceiled in case
|
||||
// devicePixelRatio is a floating point number in order to ensure there is
|
||||
// enough space to draw the character to the cell.
|
||||
// Calculate the scaled character height. Height is ceiled in case devicePixelRatio is a
|
||||
// floating point number in order to ensure there is enough space to draw the character to the
|
||||
// cell.
|
||||
this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled cell height, if lineHeight is not 1 then the value
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
// Calculate the scaled cell height, if lineHeight is _not_ 1, the resulting value will be
|
||||
// floored since lineHeight can never be lower then 1, this guarentees the scaled cell height
|
||||
// will always be larger than scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
|
||||
// correctly within the cell.
|
||||
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
// Calculate the x offset with a cell that text should draw from in order for it to be centered
|
||||
// correctly within the cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
// Recalculate the canvas dimensions, the scaled dimensions define the actual number of pixel in
|
||||
// the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
|
||||
|
||||
// The the size of the canvas on the page. It's very important that this
|
||||
// rounds to nearest integer and not ceils as browsers often set
|
||||
// window.devicePixelRatio as something like 1.100000023841858, when it's
|
||||
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
|
||||
// pixel too large for the canvas element size.
|
||||
// The the size of the canvas on the page. It's important that this rounds to nearest integer
|
||||
// and not ceils as browsers often have floating point precision issues where
|
||||
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
||||
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
|
||||
// large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
|
||||
|
||||
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio;
|
||||
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio;
|
||||
|
||||
// Get the _actual_ dimensions of an individual cell. This needs to be
|
||||
// derived from the canvasWidth/Height calculated above which takes into
|
||||
// account window.devicePixelRatio. CharMeasure.width/height by itself is
|
||||
// insufficient when the page is not at 100% zoom level as CharMeasure is
|
||||
// measured in CSS pixels, but the actual char size on the canvas can
|
||||
// differ.
|
||||
// this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
|
||||
// This fixes 110% and 125%, not 150% or 175% though
|
||||
// Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
|
||||
// device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
|
||||
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
|
||||
// size on the canvas can differ.
|
||||
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
}
|
||||
|
||||
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
||||
if (this.dimensions.scaledCanvasWidth === width && this.dimensions.scaledCanvasHeight === height) {
|
||||
return;
|
||||
}
|
||||
this.dimensions.scaledCanvasWidth = width;
|
||||
this.dimensions.scaledCanvasHeight = height;
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
private _requestRedrawViewport(): void {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||
}
|
||||
|
||||
+2
-2
@@ -79,8 +79,8 @@
|
||||
"playwright": "^1.22.1",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-loader": "^9.1.2",
|
||||
"typescript": "^4.4.4",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "4.7",
|
||||
"utf8": "^3.0.0",
|
||||
"webpack": "^5.61.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export function observeDevicePixelDimensions(element: HTMLElement, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
|
||||
// Observe any resizes to the element and extract the actual pixel size of the element if the
|
||||
// devicePixelContentBoxSize API is supported. This allows correcting rounding errors when
|
||||
// converting between CSS pixels and device pixels which causes blurry rendering when device
|
||||
// pixel ratio is not a round number.
|
||||
let observer: ResizeObserver | undefined = new ResizeObserver((entries) => {
|
||||
const entry = entries.find((entry) => entry.target === element);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect if devicePixelContentBoxSize isn't supported by the browser
|
||||
if (!('devicePixelContentBoxSize' in entry)) {
|
||||
observer?.disconnect();
|
||||
observer = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
callback(
|
||||
entry.devicePixelContentBoxSize[0].inlineSize,
|
||||
entry.devicePixelContentBoxSize[0].blockSize
|
||||
);
|
||||
});
|
||||
observer.observe(element, { box: ['device-pixel-content-box'] } as any);
|
||||
return toDisposable(() => observer?.disconnect());
|
||||
}
|
||||
@@ -3760,10 +3760,10 @@ tr46@^3.0.0:
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
ts-loader@^9.1.2:
|
||||
version "9.2.6"
|
||||
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74"
|
||||
integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==
|
||||
ts-loader@^9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.3.1.tgz#fe25cca56e3e71c1087fe48dc67f4df8c59b22d4"
|
||||
integrity sha512-OkyShkcZTsTwyS3Kt7a4rsT/t2qvEVQuKCTg4LJmpj9fhFR7ukGdZwV6Qq3tRUkqcXtfGpPR7+hFKHCG/0d3Lw==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
enhanced-resolve "^5.0.0"
|
||||
@@ -3831,16 +3831,16 @@ typedarray-to-buffer@^3.1.5:
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@4.7:
|
||||
version "4.7.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||
|
||||
typescript@^4.2.3:
|
||||
version "4.6.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
|
||||
integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
|
||||
|
||||
typescript@^4.4.4:
|
||||
version "4.4.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
|
||||
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
|
||||
|
||||
unbox-primitive@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
|
||||
|
||||
Reference in New Issue
Block a user