diff --git a/blocksuite/affine/blocks/surface/src/renderer/canvas-renderer.ts b/blocksuite/affine/blocks/surface/src/renderer/canvas-renderer.ts index 476a4dafa0..fcac695b64 100644 --- a/blocksuite/affine/blocks/surface/src/renderer/canvas-renderer.ts +++ b/blocksuite/affine/blocks/surface/src/renderer/canvas-renderer.ts @@ -1,10 +1,12 @@ import { type Color, ColorScheme } from '@blocksuite/affine-model'; +import { FeatureFlagService } from '@blocksuite/affine-shared/services'; import { requestConnectedFrame } from '@blocksuite/affine-shared/utils'; import { DisposableGroup } from '@blocksuite/global/disposable'; import type { IBound } from '@blocksuite/global/gfx'; import { getBoundWithRotation, intersects } from '@blocksuite/global/gfx'; import type { BlockStdScope } from '@blocksuite/std'; import type { + GfxCompatibleInterface, GridManager, LayerManager, SurfaceBlockModel, @@ -43,6 +45,8 @@ export class CanvasRenderer { private readonly _disposables = new DisposableGroup(); + private readonly _turboEnabled: () => boolean; + private readonly _overlays = new Set(); private _refreshRafId: number | null = null; @@ -67,6 +71,8 @@ export class CanvasRenderer { removed: HTMLCanvasElement[]; }>(); + usePlaceholder = false; + viewport: Viewport; get stackingCanvas() { @@ -83,6 +89,12 @@ export class CanvasRenderer { this.layerManager = options.layerManager; this.grid = options.gridManager; this.provider = options.provider ?? {}; + + this._turboEnabled = () => { + const featureFlagService = options.std.get(FeatureFlagService); + return featureFlagService.getFlag('enable_turbo_renderer'); + }; + this._initViewport(); options.enableStackingCanvas = options.enableStackingCanvas ?? false; @@ -213,6 +225,19 @@ export class CanvasRenderer { }, this._container); }) ); + + this._disposables.add( + this.viewport.zooming$.subscribe(isZooming => { + const shouldRenderPlaceholders = this._turboEnabled() && isZooming; + + if (this.usePlaceholder !== shouldRenderPlaceholders) { + this.usePlaceholder = shouldRenderPlaceholders; + this.refresh(); + } + }) + ); + + this.usePlaceholder = false; } private _render() { @@ -279,23 +304,30 @@ export class CanvasRenderer { for (const element of elements) { const display = (element.display ?? true) && !element.hidden; if (display && intersects(getBoundWithRotation(element), bound)) { - const renderFn = this.std.getOptional( - ElementRendererIdentifier(element.type) - ); + if ( + this.usePlaceholder && + !(element as GfxCompatibleInterface).forceFullRender + ) { + ctx.save(); + ctx.fillStyle = 'rgba(200, 200, 200, 0.5)'; + const drawX = element.x - bound.x; + const drawY = element.y - bound.y; + ctx.fillRect(drawX, drawY, element.w, element.h); + ctx.restore(); + } else { + ctx.save(); + const renderFn = this.std.getOptional( + ElementRendererIdentifier(element.type) + ); - if (!renderFn) { - console.warn(`Cannot find renderer for ${element.type}`); - continue; + if (!renderFn) continue; + + ctx.globalAlpha = element.opacity ?? 1; + const dx = element.x - bound.x; + const dy = element.y - bound.y; + renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound); + ctx.restore(); } - - ctx.save(); - - ctx.globalAlpha = element.opacity ?? 1; - const dx = element.x - bound.x; - const dy = element.y - bound.y; - - renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound); - ctx.restore(); } } diff --git a/blocksuite/affine/gfx/connector/src/element-renderer/index.ts b/blocksuite/affine/gfx/connector/src/element-renderer/index.ts index c43bebfb7d..397114fe46 100644 --- a/blocksuite/affine/gfx/connector/src/element-renderer/index.ts +++ b/blocksuite/affine/gfx/connector/src/element-renderer/index.ts @@ -249,13 +249,20 @@ function renderLabel( const [, , w, h] = labelXYWH!; const cx = w / 2; const cy = h / 2; + + ctx.setTransform(matrix); + + if (renderer.usePlaceholder) { + ctx.fillStyle = 'rgba(200, 200, 200, 0.5)'; + ctx.fillRect(0, 0, w, h); + return; // Skip actual label rendering + } + const deltas = wrapTextDeltas(text!, font, w); const lines = deltaInsertsToChunks(deltas); const lineHeight = getLineHeight(fontFamily, fontSize, fontWeight); const textHeight = (lines.length - 1) * lineHeight * 0.5; - ctx.setTransform(matrix); - ctx.font = font; ctx.textAlign = textAlign; ctx.textBaseline = 'middle'; diff --git a/blocksuite/affine/model/src/elements/connector/connector.ts b/blocksuite/affine/model/src/elements/connector/connector.ts index 4b5d5e343c..0db5fc0071 100644 --- a/blocksuite/affine/model/src/elements/connector/connector.ts +++ b/blocksuite/affine/model/src/elements/connector/connector.ts @@ -106,6 +106,11 @@ export type ConnectorElementProps = BaseElementProps & { export class ConnectorElementModel extends GfxPrimitiveElementModel { updatingPath = false; + /** + * Connectors should always render, even during zoom. + */ + forceFullRender = true; + override get connectable() { return false as const; } diff --git a/blocksuite/affine/shared/src/services/feature-flag-service.ts b/blocksuite/affine/shared/src/services/feature-flag-service.ts index 618d7aa6d5..895e812e42 100644 --- a/blocksuite/affine/shared/src/services/feature-flag-service.ts +++ b/blocksuite/affine/shared/src/services/feature-flag-service.ts @@ -19,6 +19,7 @@ export interface BlockSuiteFlags { enable_callout: boolean; enable_edgeless_scribbled_style: boolean; enable_embed_doc_with_alias: boolean; + enable_turbo_renderer: boolean; } export class FeatureFlagService extends StoreExtension { @@ -42,6 +43,7 @@ export class FeatureFlagService extends StoreExtension { enable_callout: false, enable_edgeless_scribbled_style: false, enable_embed_doc_with_alias: false, + enable_turbo_renderer: false, }); setFlag(key: keyof BlockSuiteFlags, value: boolean) { diff --git a/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxCompatibleInterface.md b/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxCompatibleInterface.md index 1c17285ca0..c5b46284d8 100644 --- a/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxCompatibleInterface.md +++ b/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxCompatibleInterface.md @@ -26,6 +26,15 @@ The bound of the element without considering the response extension. *** +### forceFullRender? + +> `optional` **forceFullRender**: `boolean` + +Whether to disable fallback rendering for this element, e.g., during zooming. +Defaults to false (fallback to placeholder rendering is enabled). + +*** + ### lockedBySelf? > `optional` **lockedBySelf**: `boolean` diff --git a/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxGroupCompatibleInterface.md b/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxGroupCompatibleInterface.md index 6d4e2059f5..89f0bcef14 100644 --- a/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxGroupCompatibleInterface.md +++ b/blocksuite/docs/api/@blocksuite/std/gfx/interfaces/GfxGroupCompatibleInterface.md @@ -45,6 +45,19 @@ The bound of the element without considering the response extension. *** +### forceFullRender? + +> `optional` **forceFullRender**: `boolean` + +Whether to disable fallback rendering for this element, e.g., during zooming. +Defaults to false (fallback to placeholder rendering is enabled). + +#### Inherited from + +[`GfxCompatibleInterface`](GfxCompatibleInterface.md).[`forceFullRender`](GfxCompatibleInterface.md#forcefullrender) + +*** + ### lockedBySelf? > `optional` **lockedBySelf**: `boolean` diff --git a/blocksuite/framework/std/src/gfx/model/base.ts b/blocksuite/framework/std/src/gfx/model/base.ts index 4977f6f716..0b2971515e 100644 --- a/blocksuite/framework/std/src/gfx/model/base.ts +++ b/blocksuite/framework/std/src/gfx/model/base.ts @@ -84,6 +84,12 @@ export interface GfxCompatibleInterface extends IBound, GfxElementGeometry { lock(): void; unlock(): void; + + /** + * Whether to disable fallback rendering for this element, e.g., during zooming. + * Defaults to false (fallback to placeholder rendering is enabled). + */ + forceFullRender?: boolean; } /** diff --git a/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts index 091f28eeb8..011426b1b9 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/surface-model.spec.ts @@ -1,6 +1,7 @@ import type { SurfaceBlockModel } from '@blocksuite/affine/blocks/surface'; import type { BrushElementModel, + ConnectorElementModel, GroupElementModel, } from '@blocksuite/affine/model'; import { beforeEach, describe, expect, test } from 'vitest'; @@ -160,6 +161,7 @@ describe('connector', () => { expect(model.getConnectors(id).map(el => el.id)).toEqual([connector.id]); expect(model.getConnectors(id2).map(el => el.id)).toEqual([connector.id]); + expect((connector as ConnectorElementModel).forceFullRender).toBe(true); }); test('multiple connectors are supported', () => { @@ -194,6 +196,8 @@ describe('connector', () => { expect(model.getConnectors(id).map(c => c.id)).toEqual(connectors); expect(model.getConnectors(id2).map(c => c.id)).toEqual(connectors); + expect((connector as ConnectorElementModel).forceFullRender).toBe(true); + expect((connector2 as ConnectorElementModel).forceFullRender).toBe(true); }); test('should return null if connector are updated', () => { @@ -213,6 +217,11 @@ describe('connector', () => { }, }); + const connectorBeforeUpdate = model.getElementById(connectorId)!; + expect( + (connectorBeforeUpdate as ConnectorElementModel).forceFullRender + ).toBe(true); + model.updateElement(connectorId, { source: { position: [0, 0], @@ -243,6 +252,11 @@ describe('connector', () => { }, }); + const connectorBeforeDelete = model.getElementById(connectorId)!; + expect( + (connectorBeforeDelete as ConnectorElementModel).forceFullRender + ).toBe(true); + model.deleteElement(connectorId); await wait(); diff --git a/packages/frontend/core/src/modules/feature-flag/constant.ts b/packages/frontend/core/src/modules/feature-flag/constant.ts index fdb9c83e65..d4755e6679 100644 --- a/packages/frontend/core/src/modules/feature-flag/constant.ts +++ b/packages/frontend/core/src/modules/feature-flag/constant.ts @@ -232,7 +232,8 @@ export const AFFINE_FLAGS = { defaultState: false, }, enable_turbo_renderer: { - category: 'affine', + category: 'blocksuite', + bsFlag: 'enable_turbo_renderer', displayName: 'Enable Turbo Renderer', description: 'Enable experimental edgeless turbo renderer', configurable: isCanaryBuild,