Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions lib/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with the width and height properties, consider updating the comment for baseline to explicitly mention that it is also an integer multiple of the physical pixel size (1/devicePixelRatio).

Suggested change
baseline: number; // Distance from top to text baseline in CSS pixels
baseline: number; // Distance from top to text baseline in CSS pixels (multiple of 1/devicePixelRatio)

}

// ============================================================================
Expand Down Expand Up @@ -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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The devicePixelRatio is captured during the constructor and stored in this.devicePixelRatio. However, browser zoom levels or moving the window between monitors with different DPIs can change window.devicePixelRatio at runtime. Since measureFont (and remeasureFont) uses the cached this.devicePixelRatio, the font metrics will become misaligned if the system DPR changes, causing the seams to reappear. Consider adding a mechanism to update this.devicePixelRatio when the resolution changes or refreshing it from window.devicePixelRatio within measureFont if it wasn't explicitly fixed in the constructor options.

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 };
}
Expand Down