mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 10:22:55 +08:00
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
This commit is contained in:
@@ -64,9 +64,6 @@ globalStyle(`${edgelessNoteContainer} > doc-title`, {
|
|||||||
|
|
||||||
globalStyle(`${edgelessNoteContainer} > doc-title .doc-title-container`, {
|
globalStyle(`${edgelessNoteContainer} > doc-title .doc-title-container`, {
|
||||||
padding: '26px 0px',
|
padding: '26px 0px',
|
||||||
fontSize: cssVar('fontTitle'),
|
|
||||||
fontWeight: 700,
|
|
||||||
lineHeight: '44px',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pageContent = style({
|
export const pageContent = style({
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ export type LastProps = z.infer<typeof NodePropsSchema>;
|
|||||||
export type LastPropsKey = keyof LastProps;
|
export type LastPropsKey = keyof LastProps;
|
||||||
|
|
||||||
const SessionPropsSchema = z.object({
|
const SessionPropsSchema = z.object({
|
||||||
|
templateCache: z.string(),
|
||||||
|
remoteColor: z.string(),
|
||||||
|
showBidirectional: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const LocalPropsSchema = z.object({
|
||||||
viewport: z.union([
|
viewport: z.union([
|
||||||
z.object({
|
z.object({
|
||||||
centerX: z.number(),
|
centerX: z.number(),
|
||||||
@@ -34,12 +40,6 @@ const SessionPropsSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
templateCache: z.string(),
|
|
||||||
remoteColor: z.string(),
|
|
||||||
showBidirectional: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const LocalPropsSchema = z.object({
|
|
||||||
presentBlackBackground: z.boolean(),
|
presentBlackBackground: z.boolean(),
|
||||||
presentFillScreen: z.boolean(),
|
presentFillScreen: z.boolean(),
|
||||||
presentHideToolbar: z.boolean(),
|
presentHideToolbar: z.boolean(),
|
||||||
|
|||||||
@@ -6,19 +6,25 @@ import {
|
|||||||
EdgelessLegacySlotIdentifier,
|
EdgelessLegacySlotIdentifier,
|
||||||
normalizeWheelDeltaY,
|
normalizeWheelDeltaY,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import type {
|
|
||||||
RootBlockModel,
|
|
||||||
ShapeElementModel,
|
|
||||||
} from '@blocksuite/affine-model';
|
|
||||||
import {
|
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,
|
EditorSettingProvider,
|
||||||
EditPropsStore,
|
EditPropsStore,
|
||||||
|
FeatureFlagService,
|
||||||
FontLoaderService,
|
FontLoaderService,
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import type { Viewport } from '@blocksuite/affine-shared/types';
|
import type { Viewport } from '@blocksuite/affine-shared/types';
|
||||||
import {
|
import {
|
||||||
isTouchPadPinchEvent,
|
isTouchPadPinchEvent,
|
||||||
|
matchFlavours,
|
||||||
requestConnectedFrame,
|
requestConnectedFrame,
|
||||||
requestThrottledConnectedFrame,
|
requestThrottledConnectedFrame,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
@@ -340,11 +346,65 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
|||||||
private _initViewport() {
|
private _initViewport() {
|
||||||
const { std, gfx } = this;
|
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 run = () => {
|
||||||
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
|
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
|
||||||
|
|
||||||
if (!storedViewport) {
|
if (!storedViewport) {
|
||||||
this.gfx.fitToScreen();
|
const enablePageBlock = this.std
|
||||||
|
.get(FeatureFlagService)
|
||||||
|
.getFlag('enable_page_block');
|
||||||
|
|
||||||
|
if (!(enablePageBlock && pageBlockViewportFitAnimation())) {
|
||||||
|
this.gfx.fitToScreen();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -365,14 +365,13 @@ export class Viewport {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
smoothTranslate(x: number, y: number) {
|
smoothTranslate(x: number, y: number, numSteps = 10) {
|
||||||
const { center } = this;
|
const { center } = this;
|
||||||
const delta = { x: x - center.x, y: y - center.y };
|
const delta = { x: x - center.x, y: y - center.y };
|
||||||
const innerSmoothTranslate = () => {
|
const innerSmoothTranslate = () => {
|
||||||
if (this._rafId) cancelAnimationFrame(this._rafId);
|
if (this._rafId) cancelAnimationFrame(this._rafId);
|
||||||
this._rafId = requestAnimationFrame(() => {
|
this._rafId = requestAnimationFrame(() => {
|
||||||
const rate = 10;
|
const step = { x: delta.x / numSteps, y: delta.y / numSteps };
|
||||||
const step = { x: delta.x / rate, y: delta.y / rate };
|
|
||||||
const nextCenter = {
|
const nextCenter = {
|
||||||
x: this.centerX + step.x,
|
x: this.centerX + step.x,
|
||||||
y: this.centerY + step.y,
|
y: this.centerY + step.y,
|
||||||
@@ -389,15 +388,14 @@ export class Viewport {
|
|||||||
innerSmoothTranslate();
|
innerSmoothTranslate();
|
||||||
}
|
}
|
||||||
|
|
||||||
smoothZoom(zoom: number, focusPoint?: IPoint) {
|
smoothZoom(zoom: number, focusPoint?: IPoint, numSteps = 10) {
|
||||||
const delta = zoom - this.zoom;
|
const delta = zoom - this.zoom;
|
||||||
if (this._rafId) cancelAnimationFrame(this._rafId);
|
if (this._rafId) cancelAnimationFrame(this._rafId);
|
||||||
|
|
||||||
const innerSmoothZoom = () => {
|
const innerSmoothZoom = () => {
|
||||||
this._rafId = requestAnimationFrame(() => {
|
this._rafId = requestAnimationFrame(() => {
|
||||||
const sign = delta > 0 ? 1 : -1;
|
const sign = delta > 0 ? 1 : -1;
|
||||||
const total = 10;
|
const step = delta / numSteps;
|
||||||
const step = delta / total;
|
|
||||||
const nextZoom = cutoff(this.zoom + step, zoom, sign);
|
const nextZoom = cutoff(this.zoom + step, zoom, sign);
|
||||||
|
|
||||||
this.setZoom(nextZoom, focusPoint);
|
this.setZoom(nextZoom, focusPoint);
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ export class Bound implements IBound {
|
|||||||
|
|
||||||
y: number;
|
y: number;
|
||||||
|
|
||||||
get bl() {
|
get bl(): IVec {
|
||||||
return [this.x, this.y + this.h];
|
return [this.x, this.y + this.h];
|
||||||
}
|
}
|
||||||
|
|
||||||
get br() {
|
get br(): IVec {
|
||||||
return [this.x + this.w, this.y + this.h];
|
return [this.x + this.w, this.y + this.h];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export class Bound implements IBound {
|
|||||||
return [this.x, this.y];
|
return [this.x, this.y];
|
||||||
}
|
}
|
||||||
|
|
||||||
get tr() {
|
get tr(): IVec {
|
||||||
return [this.x + this.w, this.y];
|
return [this.x + this.w, this.y];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ test.describe('edgeless note element toolbar', () => {
|
|||||||
await locateModeSwitchButton(page, 'page').click();
|
await locateModeSwitchButton(page, 'page').click();
|
||||||
expect(notes).toHaveCount(2);
|
expect(notes).toHaveCount(2);
|
||||||
|
|
||||||
await locateModeSwitchButton(page, 'edgeless').click();
|
await clickEdgelessModeButton(page);
|
||||||
await clickView(page, [100, 100]);
|
await clickView(page, [100, 100]);
|
||||||
await displayInPage.click();
|
await displayInPage.click();
|
||||||
await locateModeSwitchButton(page, 'page').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 undoButton = page.getByTestId('undo-display-in-page');
|
||||||
const viewTocButton = page.getByTestId('view-in-toc');
|
const viewTocButton = page.getByTestId('view-in-toc');
|
||||||
|
|
||||||
await locateModeSwitchButton(page, 'edgeless').click();
|
await clickEdgelessModeButton(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickView(page, [100, 100]);
|
await clickView(page, [100, 100]);
|
||||||
await displayInPage.click();
|
await displayInPage.click();
|
||||||
@@ -259,7 +259,7 @@ test.describe('edgeless note element toolbar', () => {
|
|||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
expect(notes).toHaveCount(1);
|
expect(notes).toHaveCount(1);
|
||||||
|
|
||||||
await locateModeSwitchButton(page, 'edgeless').click();
|
await clickEdgelessModeButton(page);
|
||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickView(page, [100, 100]);
|
await clickView(page, [100, 100]);
|
||||||
await displayInPage.click();
|
await displayInPage.click();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
createEdgelessNoteBlock,
|
createEdgelessNoteBlock,
|
||||||
focusDocTitle,
|
focusDocTitle,
|
||||||
getEdgelessSelectedIds,
|
getEdgelessSelectedIds,
|
||||||
|
getViewportCenter,
|
||||||
locateElementToolbar,
|
locateElementToolbar,
|
||||||
|
setViewportCenter,
|
||||||
} from '@affine-test/kit/utils/editor';
|
} from '@affine-test/kit/utils/editor';
|
||||||
import {
|
import {
|
||||||
pressBackspace,
|
pressBackspace,
|
||||||
@@ -207,7 +209,8 @@ test.describe('TOC display', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
await clickEdgelessModeButton(page);
|
await clickEdgelessModeButton(page);
|
||||||
const toc = await openTocPanel(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');
|
const card = locateCards(toc, 'edgeless');
|
||||||
await changeNoteDisplayMode(card, 'doc');
|
await changeNoteDisplayMode(card, 'doc');
|
||||||
|
|
||||||
@@ -337,6 +340,7 @@ test.describe('TOC and edgeless selection', () => {
|
|||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await clickEdgelessModeButton(page);
|
await clickEdgelessModeButton(page);
|
||||||
|
await setViewportCenter(page, [0, 0]);
|
||||||
await selectAllByKeyboard(page);
|
await selectAllByKeyboard(page);
|
||||||
await pressBackspace(page);
|
await pressBackspace(page);
|
||||||
await createEdgelessNoteBlock(page, [100, 100]);
|
await createEdgelessNoteBlock(page, [100, 100]);
|
||||||
@@ -490,6 +494,7 @@ test.describe('advanced visibility control', () => {
|
|||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await clickEdgelessModeButton(page);
|
await clickEdgelessModeButton(page);
|
||||||
|
await setViewportCenter(page, [0, 0]);
|
||||||
await createEdgelessNoteBlock(page, [100, 100]);
|
await createEdgelessNoteBlock(page, [100, 100]);
|
||||||
await type(page, 'hello');
|
await type(page, 'hello');
|
||||||
await clickView(page, [200, 200]);
|
await clickView(page, [200, 200]);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test } from '@affine-test/kit/playwright';
|
import { test } from '@affine-test/kit/playwright';
|
||||||
import { locateModeSwitchButton } from '@affine-test/kit/utils/editor';
|
import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor';
|
||||||
import {
|
import {
|
||||||
pasteByKeyboard,
|
pasteByKeyboard,
|
||||||
writeTextToClipboard,
|
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 page.keyboard.press('Enter');
|
||||||
|
|
||||||
await locateModeSwitchButton(page, 'edgeless').click();
|
await clickEdgelessModeButton(page);
|
||||||
|
|
||||||
const note = page.locator('affine-edgeless-note');
|
const note = page.locator('affine-edgeless-note');
|
||||||
const noteBoundingBox = await note.boundingBox();
|
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 page.keyboard.press('Enter');
|
||||||
|
|
||||||
await locateModeSwitchButton(page, 'edgeless').click();
|
await clickEdgelessModeButton(page);
|
||||||
|
|
||||||
const note = page.locator('affine-edgeless-note');
|
const note = page.locator('affine-edgeless-note');
|
||||||
const noteBoundingBox = await note.boundingBox();
|
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());
|
const url = new URL(page.url());
|
||||||
|
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
await locateModeSwitchButton(page, 'edgeless').click();
|
await clickEdgelessModeButton(page);
|
||||||
|
|
||||||
await page.mouse.move(x, y);
|
await page.mouse.move(x, y);
|
||||||
await writeTextToClipboard(page, url.toString());
|
await writeTextToClipboard(page, url.toString());
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export async function ensureInPageMode(page: Page) {
|
|||||||
|
|
||||||
export async function ensureInEdgelessMode(page: Page) {
|
export async function ensureInEdgelessMode(page: Page) {
|
||||||
await expect(locateModeSwitchButton(page, 'edgeless', true)).toBeVisible();
|
await expect(locateModeSwitchButton(page, 'edgeless', true)).toBeVisible();
|
||||||
|
// wait zoom animation
|
||||||
|
await page.waitForTimeout(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPageMode(page: Page): Promise<'page' | 'edgeless'> {
|
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
|
* Convert a canvas point to view coordinate
|
||||||
* @param point the coordinate on the canvas
|
* @param point the coordinate on the canvas
|
||||||
|
|||||||
@@ -33,13 +33,6 @@ export async function waitForAllPagesLoad(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function clickNewPageButton(page: Page, title?: string) {
|
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
|
// fixme(himself65): if too fast, the page will crash
|
||||||
await page.getByTestId('sidebar-new-page-button').click({
|
await page.getByTestId('sidebar-new-page-button').click({
|
||||||
delay: 100,
|
delay: 100,
|
||||||
|
|||||||
Reference in New Issue
Block a user