Skip to content

Commit 2f64784

Browse files
diegosouzapwtommyme
andauthored
fix(renderer): align font metrics to device pixel boundaries to prevent seams (#6)
When devicePixelRatio is non-integer (e.g. 1.25, 1.5, 1.75 from browser zoom or HiDPI displays), rounding cell width/height to the nearest CSS pixel with Math.ceil() produces fractional *physical* pixel coordinates at cell edges. The canvas rasterizer antialiases clearRect/fillRect calls at those sub-pixel boundaries. With alpha:true on the canvas (enabled in coder#93 for transparent backgrounds), the resulting partially-transparent edge pixels composite against the page background and appear as thin black seams between rows and columns. Fix: round up to the nearest *device* pixel instead of CSS pixel. The +2/+1 paddings for glyph overflow stay in CSS units before the DPR multiplication so they scale correctly. Ports only the font-metrics subset of upstream PR coder#146 — the rest of that PR bundles a substantial render-loop refactor (startRenderLoop → scheduleRender) and several perf caches whose risk/benefit needs separate evaluation against our current architecture. Inspired-by: coder#146 Co-authored-by: tommyme <chris.b.you@qq.com>
1 parent 453309b commit 2f64784

1 file changed

Lines changed: 19 additions & 8 deletions

File tree

lib/renderer.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export interface RendererOptions {
5151
}
5252

5353
export interface FontMetrics {
54-
width: number; // Character cell width in CSS pixels
55-
height: number; // Character cell height in CSS pixels
56-
baseline: number; // Distance from top to text baseline
54+
width: number; // Character cell width in CSS pixels (multiple of 1/devicePixelRatio)
55+
height: number; // Character cell height in CSS pixels (multiple of 1/devicePixelRatio)
56+
baseline: number; // Distance from top to text baseline in CSS pixels
5757
}
5858

5959
// ============================================================================
@@ -197,16 +197,27 @@ export class CanvasRenderer {
197197

198198
// Measure width using 'M' (typically widest character)
199199
const widthMetrics = ctx.measureText('M');
200-
const width = Math.ceil(widthMetrics.width);
201200

202201
// Measure height using ascent + descent with padding for glyph overflow
203202
const ascent = widthMetrics.actualBoundingBoxAscent || this.fontSize * 0.8;
204203
const descent = widthMetrics.actualBoundingBoxDescent || this.fontSize * 0.2;
205204

206-
// Add 2px padding to height to account for glyphs that overflow (like 'f', 'd', 'g', 'p')
207-
// and anti-aliasing pixels
208-
const height = Math.ceil(ascent + descent) + 2;
209-
const baseline = Math.ceil(ascent) + 1; // Offset baseline by half the padding
205+
// Round up to the nearest device pixel (not CSS pixel) so that cell
206+
// boundaries fall on exact physical pixel boundaries at any
207+
// devicePixelRatio. Without this, non-integer DPR values (e.g. 1.25,
208+
// 1.5, 1.75 from browser zoom or HiDPI displays) produce fractional
209+
// physical coordinates at cell edges, which causes the canvas
210+
// rasterizer to antialias clearRect/fillRect at those edges. Combined
211+
// with alpha:true on the canvas (used for transparency support since
212+
// #93), those partially-transparent edge pixels composite against the
213+
// page background and appear as thin black seams between rows/columns.
214+
//
215+
// The +2/+1 pixel paddings stay in CSS-pixel units before the DPR
216+
// multiplication so the glyph-overflow margin scales correctly.
217+
const dpr = this.devicePixelRatio;
218+
const width = Math.ceil(widthMetrics.width * dpr) / dpr;
219+
const height = Math.ceil((ascent + descent + 2) * dpr) / dpr;
220+
const baseline = Math.ceil((ascent + 1) * dpr) / dpr;
210221

211222
return { width, height, baseline };
212223
}

0 commit comments

Comments
 (0)