Skip to content

Commit

Permalink
feat(editor): support reusing bitmap cache when panning (#10008)
Browse files Browse the repository at this point in the history
  • Loading branch information
doodlewind authored Feb 7, 2025
1 parent 3f0a9f4 commit b9ad53a
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 37 deletions.
119 changes: 83 additions & 36 deletions blocksuite/playground/examples/renderer/canvas-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export class CanvasRenderer {
private readonly editorContainer: AffineEditorContainer;
private readonly targetContainer: HTMLElement;
public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
private lastZoom: number | null = null;
private lastSection: SectionLayout | null = null;
private lastBitmap: ImageBitmap | null = null;
private lastMode: 'page' | 'edgeless' = 'edgeless';

constructor(
editorContainer: AffineEditorContainer,
Expand Down Expand Up @@ -47,21 +51,18 @@ export class CanvasRenderer {
return this.editorContainer.std.get(GfxControllerIdentifier).viewport;
}

get hostRect() {
getHostRect() {
return this.editorContainer.host!.getBoundingClientRect();
}

get hostLayout(): {
section: SectionLayout;
hostRect: DOMRect;
} {
getHostLayout() {
const paragraphBlocks = this.editorContainer.host!.querySelectorAll(
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
);

const { viewport } = this;
const zoom = this.viewport.zoom;
const hostRect = this.hostRect;
const hostRect = this.getHostRect();

let sectionMinX = Infinity;
let sectionMinY = Infinity;
Expand Down Expand Up @@ -104,6 +105,8 @@ export class CanvasRenderer {
};
});

if (paragraphs.length === 0) return null;

const sectionModelCoord = viewport.toModelCoordFromClientCoord([
sectionMinX,
sectionMinY,
Expand All @@ -121,8 +124,25 @@ export class CanvasRenderer {
return { section, hostRect };
}

public async render(toScreen = true): Promise<void> {
const { section } = this.hostLayout;
public async render(): Promise<void> {
const hostLayout = this.getHostLayout();
if (!hostLayout) return;

const { section } = hostLayout;
const currentZoom = this.viewport.zoom;

// Use bitmap cache
if (
this.lastZoom === currentZoom &&
this.lastSection &&
this.lastBitmap &&
this.lastMode === this.editorContainer.mode
) {
this.drawBitmap(this.lastBitmap, this.lastSection);
return;
}

// Need to re-render if zoom changed or no cached bitmap
this.initWorkerSize(section.rect.w, section.rect.h);

return new Promise(resolve => {
Expand All @@ -138,48 +158,75 @@ export class CanvasRenderer {
this.worker.onmessage = (e: MessageEvent) => {
const { type, bitmap } = e.data;
if (type === 'render') {
this.canvas.style.width = this.hostRect.width + 'px';
this.canvas.style.height = this.hostRect.height + 'px';
this.canvas.width = this.hostRect.width * window.devicePixelRatio;
this.canvas.height = this.hostRect.height * window.devicePixelRatio;
const hostRect = this.getHostRect();
this.canvas.style.width = hostRect.width + 'px';
this.canvas.style.height = hostRect.height + 'px';
this.canvas.width = hostRect.width * window.devicePixelRatio;
this.canvas.height = hostRect.height * window.devicePixelRatio;

if (!this.targetContainer.querySelector('canvas')) {
this.targetContainer.append(this.canvas);
}

const ctx = this.canvas.getContext('2d');
const bitmapCanvas = new OffscreenCanvas(
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
section.rect.h * window.devicePixelRatio * this.viewport.zoom
);
const bitmapCtx = bitmapCanvas.getContext('bitmaprenderer');
bitmapCtx?.transferFromImageBitmap(bitmap);

if (!toScreen) {
resolve();
return;
// Create a copy of bitmap for caching
const tempCanvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const tempCtx = tempCanvas.getContext('2d')!;
tempCtx.drawImage(bitmap, 0, 0);
const bitmapCopy = tempCanvas.transferToImageBitmap();

// Cache the current state
this.lastZoom = currentZoom;
this.lastSection = section;
this.lastMode = this.editorContainer.mode;
if (this.lastBitmap) {
this.lastBitmap.close();
}
this.lastBitmap = bitmapCopy;

const sectionViewCoord = this.viewport.toViewCoord(
section.rect.x,
section.rect.y
);

ctx?.drawImage(
bitmapCanvas,
sectionViewCoord[0] * window.devicePixelRatio,
sectionViewCoord[1] * window.devicePixelRatio,
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
section.rect.h * window.devicePixelRatio * this.viewport.zoom
);

this.drawBitmap(bitmap, section);
resolve();
}
};
});
}

private drawBitmap(bitmap: ImageBitmap, section: SectionLayout) {
const ctx = this.canvas.getContext('2d');
if (!ctx) return;

ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const bitmapCanvas = new OffscreenCanvas(
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
section.rect.h * window.devicePixelRatio * this.viewport.zoom
);
const bitmapCtx = bitmapCanvas.getContext('bitmaprenderer');
if (!bitmapCtx) return;

const tempCanvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const tempCtx = tempCanvas.getContext('2d')!;
tempCtx.drawImage(bitmap, 0, 0);
const bitmapCopy = tempCanvas.transferToImageBitmap();

bitmapCtx.transferFromImageBitmap(bitmapCopy);

const sectionViewCoord = this.viewport.toViewCoord(
section.rect.x,
section.rect.y
);

ctx.drawImage(
bitmapCanvas,
sectionViewCoord[0] * window.devicePixelRatio,
sectionViewCoord[1] * window.devicePixelRatio,
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
section.rect.h * window.devicePixelRatio * this.viewport.zoom
);
}

public destroy() {
if (this.lastBitmap) {
this.lastBitmap.close();
}
this.worker.terminate();
}
}
8 changes: 7 additions & 1 deletion blocksuite/playground/examples/renderer/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Text } from '@blocksuite/store';

import { CanvasRenderer } from './canvas-renderer.js';
Expand All @@ -10,6 +11,11 @@ function initUI() {
const toCanvasButton = document.querySelector('#to-canvas-button')!;
toCanvasButton.addEventListener('click', async () => {
await renderer.render();

const viewport = editor.std.get(GfxControllerIdentifier).viewport;
viewport.viewportUpdated.on(async () => {
await renderer.render();
});
});
const switchModeButton = document.querySelector('#switch-mode-button')!;
switchModeButton.addEventListener('click', async () => {
Expand All @@ -19,7 +25,7 @@ function initUI() {
}

function addParagraph(content: string) {
const note = doc.getBlockByFlavour('affine:note')[0];
const note = doc.getBlocksByFlavour('affine:note')[0];
const props = {
text: new Text(content),
};
Expand Down

0 comments on commit b9ad53a

Please sign in to comment.