feat(editor): support reusing bitmap cache when panning (#10008)
This commit is contained in:
@@ -13,6 +13,10 @@ export class CanvasRenderer {
|
|||||||
private readonly editorContainer: AffineEditorContainer;
|
private readonly editorContainer: AffineEditorContainer;
|
||||||
private readonly targetContainer: HTMLElement;
|
private readonly targetContainer: HTMLElement;
|
||||||
public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
|
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(
|
constructor(
|
||||||
editorContainer: AffineEditorContainer,
|
editorContainer: AffineEditorContainer,
|
||||||
@@ -47,21 +51,18 @@ export class CanvasRenderer {
|
|||||||
return this.editorContainer.std.get(GfxControllerIdentifier).viewport;
|
return this.editorContainer.std.get(GfxControllerIdentifier).viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hostRect() {
|
getHostRect() {
|
||||||
return this.editorContainer.host!.getBoundingClientRect();
|
return this.editorContainer.host!.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
get hostLayout(): {
|
getHostLayout() {
|
||||||
section: SectionLayout;
|
|
||||||
hostRect: DOMRect;
|
|
||||||
} {
|
|
||||||
const paragraphBlocks = this.editorContainer.host!.querySelectorAll(
|
const paragraphBlocks = this.editorContainer.host!.querySelectorAll(
|
||||||
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
|
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { viewport } = this;
|
const { viewport } = this;
|
||||||
const zoom = this.viewport.zoom;
|
const zoom = this.viewport.zoom;
|
||||||
const hostRect = this.hostRect;
|
const hostRect = this.getHostRect();
|
||||||
|
|
||||||
let sectionMinX = Infinity;
|
let sectionMinX = Infinity;
|
||||||
let sectionMinY = Infinity;
|
let sectionMinY = Infinity;
|
||||||
@@ -104,6 +105,8 @@ export class CanvasRenderer {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (paragraphs.length === 0) return null;
|
||||||
|
|
||||||
const sectionModelCoord = viewport.toModelCoordFromClientCoord([
|
const sectionModelCoord = viewport.toModelCoordFromClientCoord([
|
||||||
sectionMinX,
|
sectionMinX,
|
||||||
sectionMinY,
|
sectionMinY,
|
||||||
@@ -121,8 +124,25 @@ export class CanvasRenderer {
|
|||||||
return { section, hostRect };
|
return { section, hostRect };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async render(toScreen = true): Promise<void> {
|
public async render(): Promise<void> {
|
||||||
const { section } = this.hostLayout;
|
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);
|
this.initWorkerSize(section.rect.w, section.rect.h);
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@@ -138,48 +158,75 @@ export class CanvasRenderer {
|
|||||||
this.worker.onmessage = (e: MessageEvent) => {
|
this.worker.onmessage = (e: MessageEvent) => {
|
||||||
const { type, bitmap } = e.data;
|
const { type, bitmap } = e.data;
|
||||||
if (type === 'render') {
|
if (type === 'render') {
|
||||||
this.canvas.style.width = this.hostRect.width + 'px';
|
const hostRect = this.getHostRect();
|
||||||
this.canvas.style.height = this.hostRect.height + 'px';
|
this.canvas.style.width = hostRect.width + 'px';
|
||||||
this.canvas.width = this.hostRect.width * window.devicePixelRatio;
|
this.canvas.style.height = hostRect.height + 'px';
|
||||||
this.canvas.height = this.hostRect.height * window.devicePixelRatio;
|
this.canvas.width = hostRect.width * window.devicePixelRatio;
|
||||||
|
this.canvas.height = hostRect.height * window.devicePixelRatio;
|
||||||
|
|
||||||
if (!this.targetContainer.querySelector('canvas')) {
|
if (!this.targetContainer.querySelector('canvas')) {
|
||||||
this.targetContainer.append(this.canvas);
|
this.targetContainer.append(this.canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = this.canvas.getContext('2d');
|
// Create a copy of bitmap for caching
|
||||||
const bitmapCanvas = new OffscreenCanvas(
|
const tempCanvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||||
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
|
const tempCtx = tempCanvas.getContext('2d')!;
|
||||||
section.rect.h * window.devicePixelRatio * this.viewport.zoom
|
tempCtx.drawImage(bitmap, 0, 0);
|
||||||
);
|
const bitmapCopy = tempCanvas.transferToImageBitmap();
|
||||||
const bitmapCtx = bitmapCanvas.getContext('bitmaprenderer');
|
|
||||||
bitmapCtx?.transferFromImageBitmap(bitmap);
|
|
||||||
|
|
||||||
if (!toScreen) {
|
// Cache the current state
|
||||||
resolve();
|
this.lastZoom = currentZoom;
|
||||||
return;
|
this.lastSection = section;
|
||||||
|
this.lastMode = this.editorContainer.mode;
|
||||||
|
if (this.lastBitmap) {
|
||||||
|
this.lastBitmap.close();
|
||||||
}
|
}
|
||||||
|
this.lastBitmap = bitmapCopy;
|
||||||
|
|
||||||
const sectionViewCoord = this.viewport.toViewCoord(
|
this.drawBitmap(bitmap, section);
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve();
|
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() {
|
public destroy() {
|
||||||
|
if (this.lastBitmap) {
|
||||||
|
this.lastBitmap.close();
|
||||||
|
}
|
||||||
this.worker.terminate();
|
this.worker.terminate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||||
import { Text } from '@blocksuite/store';
|
import { Text } from '@blocksuite/store';
|
||||||
|
|
||||||
import { CanvasRenderer } from './canvas-renderer.js';
|
import { CanvasRenderer } from './canvas-renderer.js';
|
||||||
@@ -10,6 +11,11 @@ function initUI() {
|
|||||||
const toCanvasButton = document.querySelector('#to-canvas-button')!;
|
const toCanvasButton = document.querySelector('#to-canvas-button')!;
|
||||||
toCanvasButton.addEventListener('click', async () => {
|
toCanvasButton.addEventListener('click', async () => {
|
||||||
await renderer.render();
|
await renderer.render();
|
||||||
|
|
||||||
|
const viewport = editor.std.get(GfxControllerIdentifier).viewport;
|
||||||
|
viewport.viewportUpdated.on(async () => {
|
||||||
|
await renderer.render();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const switchModeButton = document.querySelector('#switch-mode-button')!;
|
const switchModeButton = document.querySelector('#switch-mode-button')!;
|
||||||
switchModeButton.addEventListener('click', async () => {
|
switchModeButton.addEventListener('click', async () => {
|
||||||
@@ -19,7 +25,7 @@ function initUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addParagraph(content: string) {
|
function addParagraph(content: string) {
|
||||||
const note = doc.getBlockByFlavour('affine:note')[0];
|
const note = doc.getBlocksByFlavour('affine:note')[0];
|
||||||
const props = {
|
const props = {
|
||||||
text: new Text(content),
|
text: new Text(content),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user