diff --git a/blocksuite/affine/shared/package.json b/blocksuite/affine/shared/package.json index 684d610d50..dfe905b26d 100644 --- a/blocksuite/affine/shared/package.json +++ b/blocksuite/affine/shared/package.json @@ -46,6 +46,7 @@ "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", + "tweakpane": "^4.0.5", "unified": "^11.0.5", "yjs": "^13.6.21", "zod": "^3.23.8" diff --git a/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts b/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts index 5ee60505c0..be7c13958f 100644 --- a/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts +++ b/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts @@ -31,7 +31,6 @@ const font = new FontFace( ); // @ts-expect-error worker env self.fonts && self.fonts.add(font); -font.load().catch(console.error); function getBaseline() { const fontSize = 15; @@ -57,9 +56,15 @@ class SectionPainter { 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.zoom = zoom; + this.clearBackground(); + } + + private clearBackground() { + if (!this.canvas || !this.ctx) return; + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.fillStyle = 'white'; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } paint(section: SectionLayout) { @@ -70,6 +75,8 @@ class SectionPainter { return; } + this.clearBackground(); + ctx.scale(this.zoom, this.zoom); // Track rendered positions to avoid duplicate rendering across all paragraphs and sentences @@ -105,9 +112,23 @@ class SectionPainter { } const painter = new SectionPainter(); +let fontLoaded = false; + +font + .load() + .then(() => { + fontLoaded = true; + }) + .catch(console.error); self.onmessage = async (e: MessageEvent) => { const { type, data } = e.data; + + if (!fontLoaded) { + await font.load(); + fontLoaded = true; + } + switch (type) { case 'initSection': { const { width, height, dpr, zoom } = data; @@ -115,7 +136,6 @@ self.onmessage = async (e: MessageEvent) => { break; } case 'paintSection': { - await font.load(); const { section } = data; painter.paint(section); break; diff --git a/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts b/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts index b84a9fb68c..ea53fe0d18 100644 --- a/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts +++ b/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts @@ -6,6 +6,7 @@ import { } from '@blocksuite/block-std'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { type Container, type ServiceIdentifier } from '@blocksuite/global/di'; +import { Pane } from 'tweakpane'; import { getSentenceRects, segmentSentences } from './text-utils.js'; import { type ParagraphLayout, type SectionLayout } from './types.js'; @@ -24,6 +25,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher { private lastZoom: number | null = null; private lastSection: SectionLayout | null = null; private lastBitmap: ImageBitmap | null = null; + private debugPane: Pane | null = null; constructor(std: BlockStdScope) { super(std); @@ -33,20 +35,57 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher { } override mounted() { - const targetContainer = document.querySelector('#right-column')!; - if (!targetContainer.querySelector('canvas')) { - targetContainer.append(this.canvas); + const viewportElement = document.querySelector('.affine-edgeless-viewport'); + if (viewportElement) { + viewportElement.append(this.canvas); + this.debugPane = new Pane({ container: viewportElement as HTMLElement }); + this.initTweakpane(); } + this.viewport.viewportUpdated.on(async () => { + await this.render(); + }); + + document.fonts.load('15px Inter').then(async () => { + await this.render(); + }); } override unmounted() { if (this.lastBitmap) { this.lastBitmap.close(); } + if (this.debugPane) { + this.debugPane.dispose(); + this.debugPane = null; + } this.worker.terminate(); this.canvas.remove(); } + private initTweakpane() { + if (!this.debugPane) return; + + const paneElement = this.debugPane.element; + paneElement.style.position = 'absolute'; + paneElement.style.top = '10px'; + paneElement.style.left = '10px'; + paneElement.style.width = '250px'; + + this.debugPane.title = 'Viewport Turbo Renderer'; + + const params = { + enabled: true, + }; + + this.debugPane + .addBinding(params, 'enabled', { + label: 'Enable', + }) + .on('change', ({ value }) => { + this.canvas.style.display = value ? 'block' : 'none'; + }); + } + get viewport() { return this.std.get(GfxControllerIdentifier).viewport; } @@ -56,6 +95,8 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher { } getHostLayout() { + if (!document.fonts.check('15px Inter')) return null; + const paragraphBlocks = this.std.host.querySelectorAll( '.affine-paragraph-rich-text-wrapper [data-v-text="true"]' ); @@ -169,10 +210,14 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher { 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.style.position = 'absolute'; + this.canvas.style.left = '0px'; + this.canvas.style.top = '0px'; + this.canvas.style.width = '100%'; + this.canvas.style.height = '100%'; this.canvas.width = hostRect.width * dpr; this.canvas.height = hostRect.height * dpr; + this.canvas.style.pointerEvents = 'none'; } private updateCacheState(section: SectionLayout, bitmapCopy: ImageBitmap) { @@ -225,7 +270,6 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher { public async render(): Promise { const hostLayout = this.getHostLayout(); - if (!hostLayout) return; const { section } = hostLayout; diff --git a/blocksuite/playground/examples/renderer/editor.ts b/blocksuite/playground/examples/renderer/editor.ts index cb6ef5a4f6..192f163a1b 100644 --- a/blocksuite/playground/examples/renderer/editor.ts +++ b/blocksuite/playground/examples/renderer/editor.ts @@ -12,10 +12,11 @@ presetsEffects(); export const doc = createEmptyDoc().init(); export const editor = new AffineEditorContainer(); -editor.pageSpecs = editor.pageSpecs.concat([ViewportTurboRendererExtension]); -editor.edgelessSpecs = editor.edgelessSpecs.concat([ +editor.pageSpecs = [...editor.pageSpecs, ViewportTurboRendererExtension]; +editor.edgelessSpecs = [ + ...editor.edgelessSpecs, ViewportTurboRendererExtension, -]); +]; editor.doc = doc; editor.mode = 'edgeless'; diff --git a/blocksuite/playground/examples/renderer/index.html b/blocksuite/playground/examples/renderer/index.html index 8f39feffc6..2cc387fc4b 100644 --- a/blocksuite/playground/examples/renderer/index.html +++ b/blocksuite/playground/examples/renderer/index.html @@ -15,45 +15,15 @@ background-color: var(--affine-white-90); transition: background-color 0.3s; } - #container { - display: flex; + position: relative; width: 100%; height: 100%; } - - #left-column { - flex: 1; - padding: 20px; - border-right: 1px solid #e0e0e0; - } - - #right-column { - flex: 1; - padding: 20px; - position: relative; - display: flex; - align-items: flex-end; - justify-content: center; - } - - .top-bar { - position: absolute; - top: 0; - left: 0; - padding: 5px; - } -
-
-
-
-
-
-
-
+
diff --git a/blocksuite/playground/examples/renderer/main.ts b/blocksuite/playground/examples/renderer/main.ts index c77798153e..f31b49bc83 100644 --- a/blocksuite/playground/examples/renderer/main.ts +++ b/blocksuite/playground/examples/renderer/main.ts @@ -1,59 +1,7 @@ -import { ViewportTurboRendererIdentifier } from '@blocksuite/affine-shared/viewport-renderer'; -import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; -import { nextTick } from '@blocksuite/global/utils'; import { Text } from '@blocksuite/store'; -import { Pane } from 'tweakpane'; import { doc, editor } from './editor.js'; -type DocMode = 'page' | 'edgeless'; - -async function handleToCanvasClick() { - const renderer = editor.std.get(ViewportTurboRendererIdentifier); - await renderer.render(); - const viewport = editor.std.get(GfxControllerIdentifier).viewport; - viewport.viewportUpdated.on(async () => { - await renderer.render(); - }); -} - -async function handleModeChange(mode: DocMode) { - editor.mode = mode; - await nextTick(); - - const renderer = editor.std.get(ViewportTurboRendererIdentifier); - await renderer.render(); -} - -function initUI() { - const pane = new Pane({ - container: document.querySelector('#tweakpane-container') as HTMLElement, - }); - - const params = { - mode: 'edgeless' as DocMode, - }; - - pane - .addButton({ - title: 'To Canvas', - }) - .on('click', () => { - handleToCanvasClick().catch(console.error); - }); - pane - .addBinding(params, 'mode', { - label: 'Editor Mode', - options: { - Doc: 'page', - Edgeless: 'edgeless', - }, - }) - .on('change', ({ value }) => { - handleModeChange(value as DocMode).catch(console.error); - }); -} - function addParagraph(content: string) { const note = doc.getBlocksByFlavour('affine:note')[0]; const props = { @@ -63,9 +11,7 @@ function addParagraph(content: string) { } function main() { - initUI(); - - document.querySelector('#left-column')?.append(editor); + document.querySelector('#container')?.append(editor); const firstParagraph = doc.getBlockByFlavour('affine:paragraph')[0]; doc.updateBlock(firstParagraph, { text: new Text('Renderer') }); diff --git a/yarn.lock b/yarn.lock index 79ac7f5611..492572e3b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3754,6 +3754,7 @@ __metadata: remark-math: "npm:^6.0.0" remark-parse: "npm:^11.0.0" remark-stringify: "npm:^11.0.0" + tweakpane: "npm:^4.0.5" unified: "npm:^11.0.5" vitest: "npm:3.0.5" yjs: "npm:^13.6.21" @@ -32935,7 +32936,7 @@ __metadata: languageName: node linkType: hard -"tweakpane@npm:^4.0.4": +"tweakpane@npm:^4.0.4, tweakpane@npm:^4.0.5": version: 4.0.5 resolution: "tweakpane@npm:4.0.5" checksum: 10/7719a15ce96dd2b936b277239ccb18ee6a75ed2416a6bdacfc537515d909da6edd50161b12e91441ace5243efad3a14a98fe6e5475cae2617d7647a197117e64