Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 8 additions & 0 deletions docs/engineering-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ of a pane out of view while there is room under the cap, and gives idle ones up
recently seen first only when a visible pane needs the slot. Same four round trips: 0
attaches after the first sight of each pane. Freezing is unchanged — an idle renderer
sits on a frozen pane doing nothing — so the two axes stay separate as before.
**Thin strokes shimmered while the canvas glided.** The track moves by `translateX`, and
the offset was rounded to whole CSS pixels. At this machine's 1.67× scale a whole CSS
pixel is one and two-thirds device pixels, so two frames in three landed the composited
layers between device pixels and the compositor resampled them — box-drawing lines and
underscores swam, and a glide that stopped on such a frame stayed soft at rest. The offset
is now rounded in device pixels (`snapToDevicePixels`), which at 1× and 2× is what it was.
Screencast of the same six-notch fling, variance of the Laplacian over the pane area:
moving frames averaged 128 before and 143 after; the frame at rest went from 127 to 148.

**Then every switch stuttered.** Handing the contexts back on the way out is a rule about
the *cap*, and it was paying for the cap on every switch whether or not the page was
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/canvas-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
maxScrollX,
paneRects,
scrollToReveal,
snapToDevicePixels,
type PaneRect,
type Viewport,
} from './layout-geometry'
Expand Down Expand Up @@ -164,7 +165,7 @@ export function createCanvasView(host: HTMLElement, hooks: CanvasHooks): CanvasV

function applyScroll(value: number): void {
scrollX = value
track.style.transform = `translateX(${-Math.round(value)}px)`
track.style.transform = `translateX(${-snapToDevicePixels(value, window.devicePixelRatio)}px)`
syncIndicator()
hooks.onScroll?.()
}
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/layout-geometry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
maxScrollX,
paneRects,
scrollToReveal,
snapToDevicePixels,
visiblePaneIds,
} from './layout-geometry'

Expand Down Expand Up @@ -139,3 +140,21 @@ describe('visiblePaneIds', () => {
expect(ids).not.toContain('a1')
})
})

describe('snapToDevicePixels', () => {
it('rounds to whole CSS pixels at 1× and 2×', () => {
expect(snapToDevicePixels(10.4, 1)).toBe(10)
expect(snapToDevicePixels(10.26, 2)).toBe(10.5)
})

it('lands on a device pixel at fractional scales', () => {
const dpr = 5 / 3
const snapped = snapToDevicePixels(184, dpr)
expect(Math.abs(snapped * dpr - Math.round(snapped * dpr))).toBeLessThan(1e-9)
expect(Math.abs(snapped - 184)).toBeLessThan(1 / dpr)
})

it('treats a missing ratio as 1×', () => {
expect(snapToDevicePixels(3.7, 0)).toBe(4)
})
})
13 changes: 13 additions & 0 deletions src/renderer/layout-geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,19 @@ export function scrollToReveal(
* Panes overlapping the active region: the viewport widened by one screen each
* way, so renderers are attached before they scroll into view.
*/
/**
* A CSS length that lands on a whole device pixel.
*
* The track slides by a transform, and a translation that stops between device
* pixels is resampled by the compositor: at a 1.67× or 1.5× scale most integer
* CSS offsets do, so thin strokes shimmer while the canvas glides. Rounding in
* device pixels keeps every frame crisp; at 1× and 2× it is plain rounding.
*/
export function snapToDevicePixels(cssPx: number, devicePixelRatio: number): number {
const dpr = devicePixelRatio > 0 ? devicePixelRatio : 1
return Math.round(cssPx * dpr) / dpr
}

export function visiblePaneIds(rects: readonly PaneRect[], viewport: Viewport): string[] {
const from = viewport.scrollX - viewport.width
const to = viewport.scrollX + viewport.width * 2
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/self-check/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ export function wheel(

export function trackOffset(): number {
const track = document.querySelector<HTMLElement>('.session-host:not([hidden]) .canvas-track')
const match = /translateX\((-?\d+)px\)/.exec(track?.style.transform ?? '')
// Snapped to device pixels, so at fractional scales the CSS value has decimals.
const match = /translateX\((-?\d+(?:\.\d+)?)px\)/.exec(track?.style.transform ?? '')
return match === null ? 0 : Number(match[1])
}

Expand Down
Loading