-
Notifications
You must be signed in to change notification settings - Fork 0
fix(renderer): align font metrics to device pixel boundaries to prevent seams #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,9 +51,9 @@ export interface RendererOptions { | |
| } | ||
|
|
||
| export interface FontMetrics { | ||
| width: number; // Character cell width in CSS pixels | ||
| height: number; // Character cell height in CSS pixels | ||
| baseline: number; // Distance from top to text baseline | ||
| width: number; // Character cell width in CSS pixels (multiple of 1/devicePixelRatio) | ||
| height: number; // Character cell height in CSS pixels (multiple of 1/devicePixelRatio) | ||
| baseline: number; // Distance from top to text baseline in CSS pixels | ||
| } | ||
|
|
||
| // ============================================================================ | ||
|
|
@@ -197,16 +197,27 @@ export class CanvasRenderer { | |
|
|
||
| // Measure width using 'M' (typically widest character) | ||
| const widthMetrics = ctx.measureText('M'); | ||
| const width = Math.ceil(widthMetrics.width); | ||
|
|
||
| // Measure height using ascent + descent with padding for glyph overflow | ||
| const ascent = widthMetrics.actualBoundingBoxAscent || this.fontSize * 0.8; | ||
| const descent = widthMetrics.actualBoundingBoxDescent || this.fontSize * 0.2; | ||
|
|
||
| // Add 2px padding to height to account for glyphs that overflow (like 'f', 'd', 'g', 'p') | ||
| // and anti-aliasing pixels | ||
| const height = Math.ceil(ascent + descent) + 2; | ||
| const baseline = Math.ceil(ascent) + 1; // Offset baseline by half the padding | ||
| // Round up to the nearest device pixel (not CSS pixel) so that cell | ||
| // boundaries fall on exact physical pixel boundaries at any | ||
| // devicePixelRatio. Without this, non-integer DPR values (e.g. 1.25, | ||
| // 1.5, 1.75 from browser zoom or HiDPI displays) produce fractional | ||
| // physical coordinates at cell edges, which causes the canvas | ||
| // rasterizer to antialias clearRect/fillRect at those edges. Combined | ||
| // with alpha:true on the canvas (used for transparency support since | ||
| // #93), those partially-transparent edge pixels composite against the | ||
| // page background and appear as thin black seams between rows/columns. | ||
| // | ||
| // The +2/+1 pixel paddings stay in CSS-pixel units before the DPR | ||
| // multiplication so the glyph-overflow margin scales correctly. | ||
| const dpr = this.devicePixelRatio; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| const width = Math.ceil(widthMetrics.width * dpr) / dpr; | ||
| const height = Math.ceil((ascent + descent + 2) * dpr) / dpr; | ||
| const baseline = Math.ceil((ascent + 1) * dpr) / dpr; | ||
|
|
||
| return { width, height, baseline }; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with the
widthandheightproperties, consider updating the comment forbaselineto explicitly mention that it is also an integer multiple of the physical pixel size (1/devicePixelRatio).