From fc4fe481efc96700e99bb68c42c16e28f47875a3 Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Fri, 7 Feb 2025 14:59:08 +0800 Subject: [PATCH] refactor(editor): improve worker renderer code structure (#10011) --- .../examples/renderer/canvas-renderer.ts | 151 +++++++++--------- .../playground/examples/renderer/index.html | 23 +-- .../{canvas.worker.ts => painter.worker.ts} | 49 +++--- 3 files changed, 110 insertions(+), 113 deletions(-) rename blocksuite/playground/examples/renderer/{canvas.worker.ts => painter.worker.ts} (72%) diff --git a/blocksuite/playground/examples/renderer/canvas-renderer.ts b/blocksuite/playground/examples/renderer/canvas-renderer.ts index 4a79e29c76..dc40dd002c 100644 --- a/blocksuite/playground/examples/renderer/canvas-renderer.ts +++ b/blocksuite/playground/examples/renderer/canvas-renderer.ts @@ -2,11 +2,7 @@ import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import type { AffineEditorContainer } from '@blocksuite/presets'; import { getSentenceRects, segmentSentences } from './text-utils.js'; -import { - type ParagraphLayout, - type SectionLayout, - type ViewportState, -} from './types.js'; +import { type ParagraphLayout, type SectionLayout } from './types.js'; export class CanvasRenderer { private readonly worker: Worker; @@ -25,26 +21,13 @@ export class CanvasRenderer { this.editorContainer = editorContainer; this.targetContainer = targetContainer; - this.worker = new Worker(new URL('./canvas.worker.ts', import.meta.url), { + this.worker = new Worker(new URL('./painter.worker.ts', import.meta.url), { type: 'module', }); - } - private initWorkerSize(width: number, height: number) { - const dpr = window.devicePixelRatio; - const viewport = this.editorContainer.std.get( - GfxControllerIdentifier - ).viewport; - const viewportState: ViewportState = { - zoom: viewport.zoom, - viewScale: viewport.viewScale, - viewportX: viewport.viewportX, - viewportY: viewport.viewportY, - }; - this.worker.postMessage({ - type: 'init', - data: { width, height, dpr, viewport: viewportState }, - }); + if (!this.targetContainer.querySelector('canvas')) { + this.targetContainer.append(this.canvas); + } } get viewport() { @@ -124,72 +107,74 @@ export class CanvasRenderer { return { section, hostRect }; } - public async render(): Promise { - 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); + private initSectionRenderer(width: number, height: number) { + const dpr = window.devicePixelRatio; + this.worker.postMessage({ + type: 'initSection', + data: { width, height, dpr, zoom: this.viewport.zoom }, + }); + } + private async renderSection(section: SectionLayout): Promise { return new Promise(resolve => { if (!this.worker) return; this.worker.postMessage({ - type: 'draw', - data: { - section, - }, + type: 'paintSection', + data: { section }, }); this.worker.onmessage = (e: MessageEvent) => { - const { type, bitmap } = e.data; - if (type === 'render') { - 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); - } - - // 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; - - this.drawBitmap(bitmap, section); - resolve(); + if (e.data.type === 'bitmapPainted') { + this.handlePaintedBitmap(e.data.bitmap, section, resolve); } }; }); } + private handlePaintedBitmap( + bitmap: ImageBitmap, + section: SectionLayout, + resolve: () => void + ) { + const tempCanvas = new OffscreenCanvas(bitmap.width, bitmap.height); + const tempCtx = tempCanvas.getContext('2d')!; + tempCtx.drawImage(bitmap, 0, 0); + const bitmapCopy = tempCanvas.transferToImageBitmap(); + + this.updateCacheState(section, bitmapCopy); + this.drawBitmap(bitmap, section); + resolve(); + } + + private syncCanvasSize() { + const hostRect = this.getHostRect(); + const dpr = window.devicePixelRatio; + this.canvas.style.width = `${hostRect.width}px`; + this.canvas.style.height = `${hostRect.height}px`; + this.canvas.width = hostRect.width * dpr; + this.canvas.height = hostRect.height * dpr; + } + + private updateCacheState(section: SectionLayout, bitmapCopy: ImageBitmap) { + this.lastZoom = this.viewport.zoom; + this.lastSection = section; + this.lastMode = this.editorContainer.mode; + if (this.lastBitmap) { + this.lastBitmap.close(); + } + this.lastBitmap = bitmapCopy; + } + + private canUseCache(currentZoom: number): boolean { + return ( + this.lastZoom === currentZoom && + !!this.lastSection && + !!this.lastBitmap && + this.lastMode === this.editorContainer.mode + ); + } + private drawBitmap(bitmap: ImageBitmap, section: SectionLayout) { const ctx = this.canvas.getContext('2d'); if (!ctx) return; @@ -223,6 +208,22 @@ export class CanvasRenderer { ); } + public async render(): Promise { + const hostLayout = this.getHostLayout(); + if (!hostLayout) return; + + const { section } = hostLayout; + const currentZoom = this.viewport.zoom; + + if (this.canUseCache(currentZoom)) { + this.drawBitmap(this.lastBitmap!, this.lastSection!); + } else { + this.syncCanvasSize(); + this.initSectionRenderer(section.rect.w, section.rect.h); + await this.renderSection(section); + } + } + public destroy() { if (this.lastBitmap) { this.lastBitmap.close(); diff --git a/blocksuite/playground/examples/renderer/index.html b/blocksuite/playground/examples/renderer/index.html index 1191b7771f..f2a8b19629 100644 --- a/blocksuite/playground/examples/renderer/index.html +++ b/blocksuite/playground/examples/renderer/index.html @@ -37,16 +37,15 @@ justify-content: center; } - #to-canvas-button { + .top-bar { position: absolute; - top: 5px; - left: 5px; - } - - #switch-mode-button { - position: absolute; - top: 5px; - left: 85px; + top: 0; + left: 0; + padding: 5px; + display: flex; + gap: 5px; + justify-content: flex-start; + align-items: center; } @@ -54,8 +53,10 @@
- - +
+ + +
diff --git a/blocksuite/playground/examples/renderer/canvas.worker.ts b/blocksuite/playground/examples/renderer/painter.worker.ts similarity index 72% rename from blocksuite/playground/examples/renderer/canvas.worker.ts rename to blocksuite/playground/examples/renderer/painter.worker.ts index 1b442c6000..034717f24f 100644 --- a/blocksuite/playground/examples/renderer/canvas.worker.ts +++ b/blocksuite/playground/examples/renderer/painter.worker.ts @@ -1,23 +1,23 @@ -import { type SectionLayout, type ViewportState } from './types.js'; +import { type SectionLayout } from './types.js'; type WorkerMessageInit = { - type: 'init'; + type: 'initSection'; data: { width: number; height: number; dpr: number; - viewport: ViewportState; + zoom: number; }; }; -type WorkerMessageDraw = { - type: 'draw'; +type WorkerMessagePaint = { + type: 'paintSection'; data: { section: SectionLayout; }; }; -type WorkerMessage = WorkerMessageInit | WorkerMessageDraw; +type WorkerMessage = WorkerMessageInit | WorkerMessagePaint; const meta = { emSize: 2048, @@ -45,33 +45,28 @@ function getBaseline() { return y; } -class CanvasWorkerManager { +/** Section painter in worker */ +class SectionPainter { private canvas: OffscreenCanvas | null = null; private ctx: OffscreenCanvasRenderingContext2D | null = null; - private viewport: ViewportState | null = null; + private zoom = 1; - init( - modelWidth: number, - modelHeight: number, - dpr: number, - viewport: ViewportState - ) { - const width = modelWidth * dpr * viewport.zoom; - const height = modelHeight * dpr * viewport.zoom; + init(modelWidth: number, modelHeight: number, dpr: number, zoom: number) { + const width = modelWidth * dpr * zoom; + const height = modelHeight * dpr * zoom; this.canvas = new OffscreenCanvas(width, height); this.ctx = this.canvas.getContext('2d')!; this.ctx.scale(dpr, dpr); this.ctx.fillStyle = 'lightgrey'; this.ctx.fillRect(0, 0, width, height); - this.viewport = viewport; + this.zoom = zoom; } - draw(section: SectionLayout) { + paint(section: SectionLayout) { const { canvas, ctx } = this; if (!canvas || !ctx) return; - const zoom = this.viewport!.zoom; - ctx.scale(zoom, zoom); + ctx.scale(this.zoom, this.zoom); // Track rendered positions to avoid duplicate rendering across all paragraphs and sentences const renderedPositions = new Set(); @@ -101,24 +96,24 @@ class CanvasWorkerManager { }); const bitmap = canvas.transferToImageBitmap(); - self.postMessage({ type: 'render', bitmap }, { transfer: [bitmap] }); + self.postMessage({ type: 'bitmapPainted', bitmap }, { transfer: [bitmap] }); } } -const manager = new CanvasWorkerManager(); +const painter = new SectionPainter(); self.onmessage = async (e: MessageEvent) => { const { type, data } = e.data; switch (type) { - case 'init': { - const { width, height, dpr, viewport } = data; - manager.init(width, height, dpr, viewport); + case 'initSection': { + const { width, height, dpr, zoom } = data; + painter.init(width, height, dpr, zoom); break; } - case 'draw': { + case 'paintSection': { await font.load(); const { section } = data; - manager.draw(section); + painter.paint(section); break; } }