From 9f56a21d8a1c5d12b30ee059ca6163eb6edfc25d Mon Sep 17 00:00:00 2001 From: L-Sun Date: Mon, 10 Feb 2025 07:41:10 +0000 Subject: [PATCH] feat(editor): add animation for switching to edgeless mode firstly (#10021) Close [BS-2327](https://linear.app/affine-design/issue/BS-2327/page-block-%E5%9C%A8-edgeless-%E5%88%87%E6%8D%A2%E7%BC%A9%E6%94%BE%E5%8A%A8%E7%94%BB) ### What Changes: - Add a zoom animation when switching to edgeless mode firstly - Move viewport record from `sessionStorage` to `localStorage` https://github.com/user-attachments/assets/dac11aab-76bd-44b1-8c0e-4a8a10919841 --- .../block-note/src/note-edgeless-block.css.ts | 3 - .../shared/src/services/edit-props-store.ts | 12 ++-- .../edgeless/edgeless-root-block.ts | 72 +++++++++++++++++-- .../framework/block-std/src/gfx/viewport.ts | 10 ++- .../framework/global/src/utils/model/bound.ts | 6 +- .../e2e/blocksuite/edgeless/note.spec.ts | 6 +- .../blocksuite/outline/outline-panel.spec.ts | 7 +- tests/affine-local/e2e/links.spec.ts | 8 +-- tests/kit/src/utils/editor.ts | 28 ++++++++ tests/kit/src/utils/page-logic.ts | 7 -- 10 files changed, 120 insertions(+), 39 deletions(-) diff --git a/blocksuite/affine/block-note/src/note-edgeless-block.css.ts b/blocksuite/affine/block-note/src/note-edgeless-block.css.ts index 3d2e3d8a21..ea5ad8bd9c 100644 --- a/blocksuite/affine/block-note/src/note-edgeless-block.css.ts +++ b/blocksuite/affine/block-note/src/note-edgeless-block.css.ts @@ -64,9 +64,6 @@ globalStyle(`${edgelessNoteContainer} > doc-title`, { globalStyle(`${edgelessNoteContainer} > doc-title .doc-title-container`, { padding: '26px 0px', - fontSize: cssVar('fontTitle'), - fontWeight: 700, - lineHeight: '44px', }); export const pageContent = style({ diff --git a/blocksuite/affine/shared/src/services/edit-props-store.ts b/blocksuite/affine/shared/src/services/edit-props-store.ts index c59f044a00..d05f204c8c 100644 --- a/blocksuite/affine/shared/src/services/edit-props-store.ts +++ b/blocksuite/affine/shared/src/services/edit-props-store.ts @@ -21,6 +21,12 @@ export type LastProps = z.infer; export type LastPropsKey = keyof LastProps; const SessionPropsSchema = z.object({ + templateCache: z.string(), + remoteColor: z.string(), + showBidirectional: z.boolean(), +}); + +const LocalPropsSchema = z.object({ viewport: z.union([ z.object({ centerX: z.number(), @@ -34,12 +40,6 @@ const SessionPropsSchema = z.object({ .optional(), }), ]), - templateCache: z.string(), - remoteColor: z.string(), - showBidirectional: z.boolean(), -}); - -const LocalPropsSchema = z.object({ presentBlackBackground: z.boolean(), presentFillScreen: z.boolean(), presentHideToolbar: z.boolean(), diff --git a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts index 02a27cdb1a..10657b0cb6 100644 --- a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts +++ b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts @@ -6,19 +6,25 @@ import { EdgelessLegacySlotIdentifier, normalizeWheelDeltaY, } from '@blocksuite/affine-block-surface'; -import type { - RootBlockModel, - ShapeElementModel, -} from '@blocksuite/affine-model'; import { + type NoteBlockModel, + NoteDisplayMode, + type RootBlockModel, + type ShapeElementModel, +} from '@blocksuite/affine-model'; +import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts'; +import { + DocModeProvider, EditorSettingProvider, EditPropsStore, + FeatureFlagService, FontLoaderService, ThemeProvider, } from '@blocksuite/affine-shared/services'; import type { Viewport } from '@blocksuite/affine-shared/types'; import { isTouchPadPinchEvent, + matchFlavours, requestConnectedFrame, requestThrottledConnectedFrame, } from '@blocksuite/affine-shared/utils'; @@ -340,11 +346,65 @@ export class EdgelessRootBlockComponent extends BlockComponent< private _initViewport() { const { std, gfx } = this; + const pageBlockViewportFitAnimation = () => { + const primaryMode = std.get(DocModeProvider).getPrimaryMode(this.doc.id); + const note = this.model.children.find( + (child): child is NoteBlockModel => + matchFlavours(child, ['affine:note']) && + child.displayMode !== NoteDisplayMode.EdgelessOnly + ); + + if (primaryMode !== 'page' || !note || note.edgeless.collapse) + return false; + + const leftPadding = parseInt( + window + .getComputedStyle(this) + .getPropertyValue('--affine-editor-side-padding') + .replace('px', '') + ); + if (isNaN(leftPadding)) return false; + + let editorWidth = parseInt( + window + .getComputedStyle(this) + .getPropertyValue('--affine-editor-width') + .replace('px', '') + ); + if (isNaN(editorWidth)) return false; + + const containerWidth = this.getBoundingClientRect().width; + const leftMargin = + containerWidth > editorWidth ? (containerWidth - editorWidth) / 2 : 0; + + const pageTitleAnchor = gfx.viewport.toModelCoord( + leftPadding + leftMargin, + 0 + ); + + const noteBound = Bound.deserialize(note.xywh); + const edgelessTitleAnchor = Vec.add(noteBound.tl, [ + EDGELESS_BLOCK_CHILD_PADDING, + 12, + ]); + + const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor); + gfx.viewport.setCenter(center[0], center[1]); + gfx.viewport.smoothZoom(0.65, undefined, 15); + + return true; + }; + const run = () => { const storedViewport = std.get(EditPropsStore).getStorage('viewport'); - if (!storedViewport) { - this.gfx.fitToScreen(); + const enablePageBlock = this.std + .get(FeatureFlagService) + .getFlag('enable_page_block'); + + if (!(enablePageBlock && pageBlockViewportFitAnimation())) { + this.gfx.fitToScreen(); + } return; } diff --git a/blocksuite/framework/block-std/src/gfx/viewport.ts b/blocksuite/framework/block-std/src/gfx/viewport.ts index 68513f2aeb..187b2eaf3a 100644 --- a/blocksuite/framework/block-std/src/gfx/viewport.ts +++ b/blocksuite/framework/block-std/src/gfx/viewport.ts @@ -365,14 +365,13 @@ export class Viewport { }); } - smoothTranslate(x: number, y: number) { + smoothTranslate(x: number, y: number, numSteps = 10) { const { center } = this; const delta = { x: x - center.x, y: y - center.y }; const innerSmoothTranslate = () => { if (this._rafId) cancelAnimationFrame(this._rafId); this._rafId = requestAnimationFrame(() => { - const rate = 10; - const step = { x: delta.x / rate, y: delta.y / rate }; + const step = { x: delta.x / numSteps, y: delta.y / numSteps }; const nextCenter = { x: this.centerX + step.x, y: this.centerY + step.y, @@ -389,15 +388,14 @@ export class Viewport { innerSmoothTranslate(); } - smoothZoom(zoom: number, focusPoint?: IPoint) { + smoothZoom(zoom: number, focusPoint?: IPoint, numSteps = 10) { const delta = zoom - this.zoom; if (this._rafId) cancelAnimationFrame(this._rafId); const innerSmoothZoom = () => { this._rafId = requestAnimationFrame(() => { const sign = delta > 0 ? 1 : -1; - const total = 10; - const step = delta / total; + const step = delta / numSteps; const nextZoom = cutoff(this.zoom + step, zoom, sign); this.setZoom(nextZoom, focusPoint); diff --git a/blocksuite/framework/global/src/utils/model/bound.ts b/blocksuite/framework/global/src/utils/model/bound.ts index 277676e187..c50b955795 100644 --- a/blocksuite/framework/global/src/utils/model/bound.ts +++ b/blocksuite/framework/global/src/utils/model/bound.ts @@ -71,11 +71,11 @@ export class Bound implements IBound { y: number; - get bl() { + get bl(): IVec { return [this.x, this.y + this.h]; } - get br() { + get br(): IVec { return [this.x + this.w, this.y + this.h]; } @@ -155,7 +155,7 @@ export class Bound implements IBound { return [this.x, this.y]; } - get tr() { + get tr(): IVec { return [this.x + this.w, this.y]; } diff --git a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts index 54e70d1e1d..abf21901fe 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts @@ -236,7 +236,7 @@ test.describe('edgeless note element toolbar', () => { await locateModeSwitchButton(page, 'page').click(); expect(notes).toHaveCount(2); - await locateModeSwitchButton(page, 'edgeless').click(); + await clickEdgelessModeButton(page); await clickView(page, [100, 100]); await displayInPage.click(); await locateModeSwitchButton(page, 'page').click(); @@ -246,7 +246,7 @@ test.describe('edgeless note element toolbar', () => { const undoButton = page.getByTestId('undo-display-in-page'); const viewTocButton = page.getByTestId('view-in-toc'); - await locateModeSwitchButton(page, 'edgeless').click(); + await clickEdgelessModeButton(page); await waitForEditorLoad(page); await clickView(page, [100, 100]); await displayInPage.click(); @@ -259,7 +259,7 @@ test.describe('edgeless note element toolbar', () => { await waitForEditorLoad(page); expect(notes).toHaveCount(1); - await locateModeSwitchButton(page, 'edgeless').click(); + await clickEdgelessModeButton(page); await waitForEditorLoad(page); await clickView(page, [100, 100]); await displayInPage.click(); diff --git a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts index 11ced364ea..1bd29876e9 100644 --- a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts +++ b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts @@ -6,7 +6,9 @@ import { createEdgelessNoteBlock, focusDocTitle, getEdgelessSelectedIds, + getViewportCenter, locateElementToolbar, + setViewportCenter, } from '@affine-test/kit/utils/editor'; import { pressBackspace, @@ -207,7 +209,8 @@ test.describe('TOC display', () => { }) => { await clickEdgelessModeButton(page); const toc = await openTocPanel(page); - await createEdgelessNoteBlock(page, [100, 100]); + const viewportCenter = await getViewportCenter(page); + await createEdgelessNoteBlock(page, [viewportCenter.x, viewportCenter.y]); const card = locateCards(toc, 'edgeless'); await changeNoteDisplayMode(card, 'doc'); @@ -337,6 +340,7 @@ test.describe('TOC and edgeless selection', () => { page, }) => { await clickEdgelessModeButton(page); + await setViewportCenter(page, [0, 0]); await selectAllByKeyboard(page); await pressBackspace(page); await createEdgelessNoteBlock(page, [100, 100]); @@ -490,6 +494,7 @@ test.describe('advanced visibility control', () => { page, }) => { await clickEdgelessModeButton(page); + await setViewportCenter(page, [0, 0]); await createEdgelessNoteBlock(page, [100, 100]); await type(page, 'hello'); await clickView(page, [200, 200]); diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index 6627a875ec..4a1e5a0483 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -1,5 +1,5 @@ import { test } from '@affine-test/kit/playwright'; -import { locateModeSwitchButton } from '@affine-test/kit/utils/editor'; +import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor'; import { pasteByKeyboard, writeTextToClipboard, @@ -506,7 +506,7 @@ test('the viewport should be fit when the linked document is with edgeless mode' }) => { await page.keyboard.press('Enter'); - await locateModeSwitchButton(page, 'edgeless').click(); + await clickEdgelessModeButton(page); const note = page.locator('affine-edgeless-note'); const noteBoundingBox = await note.boundingBox(); @@ -570,7 +570,7 @@ test('should show edgeless content when switching card view of linked mode doc i }) => { await page.keyboard.press('Enter'); - await locateModeSwitchButton(page, 'edgeless').click(); + await clickEdgelessModeButton(page); const note = page.locator('affine-edgeless-note'); const noteBoundingBox = await note.boundingBox(); @@ -596,7 +596,7 @@ test('should show edgeless content when switching card view of linked mode doc i const url = new URL(page.url()); await clickNewPageButton(page); - await locateModeSwitchButton(page, 'edgeless').click(); + await clickEdgelessModeButton(page); await page.mouse.move(x, y); await writeTextToClipboard(page, url.toString()); diff --git a/tests/kit/src/utils/editor.ts b/tests/kit/src/utils/editor.ts index 1337a53efd..b68354ec9c 100644 --- a/tests/kit/src/utils/editor.ts +++ b/tests/kit/src/utils/editor.ts @@ -36,6 +36,8 @@ export async function ensureInPageMode(page: Page) { export async function ensureInEdgelessMode(page: Page) { await expect(locateModeSwitchButton(page, 'edgeless', true)).toBeVisible(); + // wait zoom animation + await page.waitForTimeout(500); } export async function getPageMode(page: Page): Promise<'page' | 'edgeless'> { @@ -80,6 +82,32 @@ export async function getEdgelessSelectedIds(page: Page, editorIndex = 0) { }); } +export async function getViewportCenter(page: Page, editorIndex = 0) { + const container = locateEditorContainer(page, editorIndex); + return container.evaluate((container: AffineEditorContainer) => { + const root = container.querySelector('affine-edgeless-root'); + if (!root) { + throw new Error('Edgeless root not found'); + } + return root.gfx.viewport.center; + }); +} + +export async function setViewportCenter( + page: Page, + center: IVec, + editorIndex = 0 +) { + const container = locateEditorContainer(page, editorIndex); + return container.evaluate((container: AffineEditorContainer, center) => { + const root = container.querySelector('affine-edgeless-root'); + if (!root) { + throw new Error('Edgeless root not found'); + } + root.gfx.viewport.setCenter(center[0], center[1]); + }, center); +} + /** * Convert a canvas point to view coordinate * @param point the coordinate on the canvas diff --git a/tests/kit/src/utils/page-logic.ts b/tests/kit/src/utils/page-logic.ts index baab2d12d5..570da552a7 100644 --- a/tests/kit/src/utils/page-logic.ts +++ b/tests/kit/src/utils/page-logic.ts @@ -33,13 +33,6 @@ export async function waitForAllPagesLoad(page: Page) { } export async function clickNewPageButton(page: Page, title?: string) { - // FiXME: when the page is in edgeless mode, clickNewPageButton will create a new edgeless page - const edgelessPage = page.locator('edgeless-editor'); - if (await edgelessPage.isVisible()) { - await page.getByTestId('switch-page-mode-button').click({ - delay: 100, - }); - } // fixme(himself65): if too fast, the page will crash await page.getByTestId('sidebar-new-page-button').click({ delay: 100,