diff --git a/packages/blocks/src/__internal__/content-parser/index.ts b/packages/blocks/src/__internal__/content-parser/index.ts index 8342782ed387..2b41577f8837 100644 --- a/packages/blocks/src/__internal__/content-parser/index.ts +++ b/packages/blocks/src/__internal__/content-parser/index.ts @@ -1,12 +1,15 @@ import { assertExists } from '@blocksuite/global/utils'; +import type { IBound } from '@blocksuite/phasor'; import type { BaseBlockModel, Page } from '@blocksuite/store'; import { Slot } from '@blocksuite/store'; import { marked } from 'marked'; +import { xywhArrayToObject } from '../..//page-block/edgeless/utils.js'; import type { PageBlockModel } from '../../models.js'; import type { EdgelessPageBlockComponent } from '../../page-block/edgeless/edgeless-page-block.js'; import { getFileFromClipboard } from '../clipboard/utils/pure.js'; import { + getBlockElementById, getEditorContainer, getPageBlock, isPageMode, @@ -76,64 +79,149 @@ export class ContentParser { ); } - public async transPageToCanvas(): Promise { + private async _checkReady() { + const promise = new Promise(resolve => { + let count = 0; + const checkReactRender = setInterval(async () => { + const root = this._page.root; + const pageBlock = root ? getPageBlock(root) : null; + const imageLoadingComponent = document.querySelector( + 'affine-image-block-loading-card' + ); + if (pageBlock && !imageLoadingComponent) { + clearInterval(checkReactRender); + resolve(true); + } + count++; + if (count > 10 * 60) { + clearInterval(checkReactRender); + resolve(false); + } + }, 100); + }); + return await promise; + } + + private async _edgelessToCanvas( + edgeless: EdgelessPageBlockComponent, + bound: IBound + ): Promise { const root = this._page.root; if (!root) return; + const html2canvas = (await import('html2canvas')).default; if (!(html2canvas instanceof Function)) return; + const container = document.querySelector( + '.affine-block-children-container' + ); + if (!container) return; + + const dpr = window.devicePixelRatio || 1; + const canvas = document.createElement('canvas'); + canvas.width = (bound.w + 100) * dpr; + canvas.height = (bound.h + 100) * dpr; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.scale(dpr, dpr); + + ctx.fillStyle = window.getComputedStyle(container).backgroundColor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const html2canvasOption = { + ignoreElements: function (element: Element) { + if ( + element.tagName === 'AFFINE-BLOCK-HUB' || + element.tagName === 'EDGELESS-TOOLBAR' || + element.classList.contains('dg') + ) { + return true; + } else { + return false; + } + }, + onclone: function (documentClone: Document, element: HTMLElement) { + // html2canvas can't support transform feature + element.style.setProperty('transform', 'none'); + }, + }; + + const nodeElements = edgeless.getSortedElementsByBound(bound); + for (const nodeElement of nodeElements) { + const blockElement = getBlockElementById(nodeElement.id)?.closest( + '.affine-edgeless-block-child' + ); + const blockBound = xywhArrayToObject(nodeElement); + const canvasData = await html2canvas( + blockElement as HTMLElement, + html2canvasOption + ); + ctx.drawImage( + canvasData, + blockBound.x - bound.x + 50, + blockBound.y - bound.y + 50, + blockBound.w, + blockBound.h + ); + } + + const surfaceCanvas = edgeless.surface.viewport.getCanvasByBound(bound); + ctx.drawImage(surfaceCanvas, 50, 50, bound.w, bound.h); + + return canvas; + } + + private async _docToCanvas(): Promise { const editorContainer = getEditorContainer(this._page); - if (isPageMode(this._page)) { - const styleElement = document.createElement('style'); - styleElement.textContent = - 'editor-container,.affine-editor-container {height: auto;}'; - editorContainer.appendChild(styleElement); + const pageContainer = editorContainer.querySelector( + '.affine-default-page-block-container' + ); + if (!pageContainer) return; + + const html2canvas = (await import('html2canvas')).default; + if (!(html2canvas instanceof Function)) return; + + const html2canvasOption = { + ignoreElements: function (element: Element) { + if ( + element.tagName === 'AFFINE-BLOCK-HUB' || + element.tagName === 'EDGELESS-TOOLBAR' || + element.classList.contains('dg') + ) { + return true; + } else { + return false; + } + }, + }; - // todo check render and image + const data = await html2canvas( + pageContainer as HTMLElement, + html2canvasOption + ); + return data; + } - const data = await html2canvas(editorContainer); - editorContainer.removeChild(styleElement); - return data; + private async _toCanvas(): Promise { + await this._checkReady(); + + if (isPageMode(this._page)) { + return await this._docToCanvas(); } else { - const styleElement = document.createElement('style'); + const root = this._page.root; + if (!root) return; + const edgeless = getPageBlock(root) as EdgelessPageBlockComponent; const bound = edgeless.getElementsBound(); assertExists(bound); - const { x, y, w, h } = bound; - styleElement.textContent = ` - edgeless-toolbar {display: none;} - editor-container,.affine-editor-container {height: ${ - h + 100 - }px; width: ${w + 100}px} - `; - editorContainer.appendChild(styleElement); - - const width = edgeless.surface.viewport.width; - const height = edgeless.surface.viewport.height; - edgeless.surface.viewport.setCenter( - x + width / 2 - 50, - y + height / 2 - 50 - ); - edgeless.surface.onResize(); - - // todo check render and image - - const promise = new Promise(resolve => { - setTimeout(async () => { - const canvasData = await html2canvas(editorContainer); - resolve(canvasData); - }, 0); - }); - const data = (await promise) as HTMLCanvasElement; - editorContainer.removeChild(styleElement); - return data; + return await this._edgelessToCanvas(edgeless, bound); } } public async exportPng() { const root = this._page.root; if (!root) return; - const canvasImage = await this.transPageToCanvas(); + const canvasImage = await this._toCanvas(); if (!canvasImage) { return; } @@ -147,7 +235,7 @@ export class ContentParser { public async exportPdf() { const root = this._page.root; if (!root) return; - const canvasImage = await this.transPageToCanvas(); + const canvasImage = await this._toCanvas(); if (!canvasImage) { return; } @@ -163,7 +251,9 @@ export class ContentParser { 0, 0, canvasImage.width, - canvasImage.height + canvasImage.height, + '', + 'FAST' ); FileExporter.exportFile( (root as PageBlockModel).title.toString() + '.pdf', diff --git a/packages/blocks/src/page-block/edgeless/edgeless-page-block.ts b/packages/blocks/src/page-block/edgeless/edgeless-page-block.ts index 82e0ce5a479b..5464f5e3308f 100644 --- a/packages/blocks/src/page-block/edgeless/edgeless-page-block.ts +++ b/packages/blocks/src/page-block/edgeless/edgeless-page-block.ts @@ -536,6 +536,12 @@ export class EdgelessPageBlockComponent }); } + getSortedElementsByBound(bound: IBound) { + return this.sortedNotes.filter(element => { + return intersects(bound, xywhArrayToObject(element)); + }); + } + // Just update `index`, we don't change the order of the notes in the children. reorderNotes = ({ elements, type }: ReorderingAction) => { const updateIndexes = (keys: string[], elements: Selectable[]) => { diff --git a/packages/phasor/src/renderer.ts b/packages/phasor/src/renderer.ts index 550758d6b9af..e1a9dea66fef 100644 --- a/packages/phasor/src/renderer.ts +++ b/packages/phasor/src/renderer.ts @@ -32,6 +32,8 @@ export interface SurfaceViewport { addOverlay(overlay: Overlay): void; removeOverlay(overlay: Overlay): void; + + getCanvasByBound(bound: IBound): HTMLCanvasElement; } /** @@ -256,7 +258,7 @@ export class Renderer implements SurfaceViewport { } private _render() { - const { ctx, gridManager, viewportBounds, width, height, rc, zoom } = this; + const { ctx, viewportBounds, width, height, rc, zoom } = this; const dpr = window.devicePixelRatio; ctx.clearRect(0, 0, width * dpr, height * dpr); @@ -264,14 +266,25 @@ export class Renderer implements SurfaceViewport { ctx.setTransform(zoom * dpr, 0, 0, zoom * dpr, 0, 0); - const elements = gridManager.search(viewportBounds); + this._renderByBound(ctx, rc, viewportBounds); + } + + private _renderByBound( + ctx: CanvasRenderingContext2D | null, + rc: RoughCanvas, + bound: IBound + ) { + if (!ctx) return; + + const { gridManager } = this; + const elements = gridManager.search(bound); for (const element of elements) { - const dx = element.x - viewportBounds.x; - const dy = element.y - viewportBounds.y; + const dx = element.x - bound.x; + const dy = element.y - bound.y; ctx.save(); ctx.translate(dx, dy); const localRecord = element.localRecord; - if (intersects(element, viewportBounds) && localRecord.display) { + if (intersects(element, bound) && localRecord.display) { ctx.globalAlpha = localRecord.opacity; element.render(ctx, rc); } @@ -281,7 +294,7 @@ export class Renderer implements SurfaceViewport { for (const overlay of this._overlays) { ctx.save(); - ctx.translate(-viewportBounds.x, -viewportBounds.y); + ctx.translate(-bound.x, -bound.y); overlay.render(ctx); ctx.restore(); } @@ -289,6 +302,21 @@ export class Renderer implements SurfaceViewport { ctx.restore(); } + public getCanvasByBound(bound: IBound): HTMLCanvasElement { + const dpr = window.devicePixelRatio || 1; + const canvas = document.createElement('canvas'); + canvas.width = bound.w * dpr; + canvas.height = bound.h * dpr; + + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + ctx.scale(dpr, dpr); + + const rc = new RoughCanvas(canvas); + this._renderByBound(ctx, rc, bound); + + return canvas; + } + public addOverlay(overlay: Overlay) { this._overlays.add(overlay); this._shouldUpdate = true;