fix(canvas): filter retained backing previews

- Keep stale scene backings available during zoom for smooth interaction

- Draw scaled backings with linear filtering to avoid blocky text artifacts
This commit is contained in:
Danila Poyarkov 2026-05-22 14:44:16 +03:00
parent a49f626e3e
commit 8f4f7a47e9
2 changed files with 77 additions and 8 deletions

View file

@ -103,6 +103,10 @@ function backingWorldCoverageContainsLiveViewport(r: SkiaRenderer): boolean {
)
}
function backingZoomMatchesLiveViewport(r: SkiaRenderer): boolean {
return Math.abs((r.sceneBacking?.zoom ?? r.zoom) - r.zoom) <= 0.0001
}
function backingCoverageContainsLiveViewport(
r: SkiaRenderer,
sceneVersion: number,
@ -110,7 +114,7 @@ function backingCoverageContainsLiveViewport(
positionPreviewVersion: number
): boolean {
if (!backingMetadataMatches(r, sceneVersion, positionPreviewVersion)) return false
const crispZoom = Math.abs((r.sceneBacking?.zoom ?? r.zoom) - r.zoom) <= 0.0001
const crispZoom = backingZoomMatchesLiveViewport(r)
if (allowStaleZoom && backingScreenCoverageContainsViewport(r)) return true
return crispZoom && backingWorldCoverageContainsLiveViewport(r)
}
@ -139,12 +143,13 @@ function drawSceneBacking(
const x = r.panX - backing.panX * scale
const y = r.panY - backing.panY * scale
r.opacityPaint.setAlphaf(1)
canvas.drawImageRect(
canvas.drawImageRectOptions(
backing.image,
r.ck.LTRBRect(0, 0, backing.width * backing.dpr, backing.height * backing.dpr),
r.ck.LTRBRect(x, y, x + backing.width * scale, y + backing.height * scale),
r.opacityPaint,
true
r.ck.FilterMode.Linear,
r.ck.MipmapMode.None,
r.opacityPaint
)
return true
}

View file

@ -17,7 +17,9 @@ function createRenderer(surfaceFactory: () => Surface | null) {
top,
right,
bottom
])
]),
FilterMode: { Linear: 'Linear' },
MipmapMode: { None: 'None' }
} as SkiaRenderer['ck'],
surface: {
makeSurface: mock(surfaceFactory)
@ -54,7 +56,8 @@ function createRenderer(surfaceFactory: () => Surface | null) {
function createCanvas() {
const canvas: Partial<Canvas> = {
drawImageRect: mock()
drawImageRect: mock(),
drawImageRectOptions: mock()
}
return canvas as Canvas
}
@ -79,10 +82,71 @@ test('retained scene backing falls back when CanvasKit cannot create an offscree
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(false)
expect(r.surface.makeSurface).toHaveBeenCalled()
expect(canvas.drawImageRect).not.toHaveBeenCalled()
expect(canvas.drawImageRectOptions).not.toHaveBeenCalled()
expect(r.sceneBacking).toBeNull()
})
test('retained scene backing filters cross-zoom previews instead of falling back to live rendering', () => {
const r = createRenderer(() => null)
r.zoom = 1
r.sceneBackingPreviewUntil = Number.POSITIVE_INFINITY
r.sceneBacking = {
image: { delete: mock() } as CKImage,
pageId: 'page',
sceneVersion: 1,
positionPreviewVersion: 0,
panX: 0,
panY: 0,
zoom: 0.5,
width: 300,
height: 300,
dpr: 1,
worldX: 0,
worldY: 0,
worldWidth: 600,
worldHeight: 600
} as NonNullable<SkiaRenderer['sceneBacking']>
const canvas = createCanvas()
const graph = createGraph()
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(true)
expect(canvas.drawImageRectOptions).toHaveBeenCalledWith(
r.sceneBacking.image,
expect.anything(),
expect.anything(),
r.ck.FilterMode.Linear,
r.ck.MipmapMode.None,
r.opacityPaint
)
})
test('retained scene backing allows same-zoom previews while panning', () => {
const r = createRenderer(() => null)
r.zoom = 1
r.sceneBackingPreviewUntil = Number.POSITIVE_INFINITY
r.sceneBacking = {
image: { delete: mock() } as CKImage,
pageId: 'page',
sceneVersion: 1,
positionPreviewVersion: 0,
panX: 0,
panY: 0,
zoom: 1,
width: 300,
height: 300,
dpr: 1,
worldX: 0,
worldY: 0,
worldWidth: 300,
worldHeight: 300
} as NonNullable<SkiaRenderer['sceneBacking']>
const canvas = createCanvas()
const graph = createGraph()
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(true)
expect(canvas.drawImageRectOptions).toHaveBeenCalled()
})
test('retained scene backing invalidates stale position-preview metadata', () => {
const r = createRenderer(() => null)
r.sceneBacking = {
@ -106,5 +170,5 @@ test('retained scene backing invalidates stale position-preview metadata', () =>
const graph = createGraph(2)
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(false)
expect(canvas.drawImageRect).not.toHaveBeenCalled()
expect(canvas.drawImageRectOptions).not.toHaveBeenCalled()
})