feat: async renderScene

This commit is contained in:
Arnošt Pleskot 2023-08-08 17:30:54 +02:00
parent f2611f5d1f
commit 9e9f564ea9
No known key found for this signature in database
4 changed files with 543 additions and 547 deletions

View File

@ -82,6 +82,7 @@ import {
isElementInFrame, isElementInFrame,
} from "../frame"; } from "../frame";
import "canvas-roundrect-polyfill"; import "canvas-roundrect-polyfill";
import { Unpromisify } from "../utility-types";
export const DEFAULT_SPACING = 2; export const DEFAULT_SPACING = 2;
@ -577,7 +578,7 @@ export const paintBackground = async (
} }
}; };
export const _renderScene = ({ export const _renderScene = async ({
elements, elements,
appState, appState,
scale, scale,
@ -591,11 +592,14 @@ export const _renderScene = ({
rc: RoughCanvas; rc: RoughCanvas;
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
renderConfig: RenderConfig; renderConfig: RenderConfig;
}) => }): Promise<{
atLeastOneVisibleElement: boolean;
scrollBars: ScrollBars | undefined;
}> =>
// extra options passed to the renderer // extra options passed to the renderer
{ {
if (canvas === null) { if (canvas === null) {
return { atLeastOneVisibleElement: false }; return { atLeastOneVisibleElement: false, scrollBars: undefined };
} }
const { const {
renderScrollbars = false, renderScrollbars = false,
@ -621,19 +625,16 @@ export const _renderScene = ({
context.filter = THEME_FILTER; context.filter = THEME_FILTER;
} }
let renderOutput: { await paintBackground(
atLeastOneVisibleElement: boolean; context,
scrollBars: ScrollBars | undefined; normalizedCanvasWidth,
} = { normalizedCanvasHeight,
atLeastOneVisibleElement: false, {
scrollBars: undefined,
};
paintBackground(context, normalizedCanvasWidth, normalizedCanvasHeight, {
isExporting, isExporting,
viewBackgroundColor, viewBackgroundColor,
exportBackgroundImage, exportBackgroundImage,
}).then(() => { },
);
// Apply zoom // Apply zoom
context.save(); context.save();
context.scale(renderConfig.zoom.value, renderConfig.zoom.value); context.scale(renderConfig.zoom.value, renderConfig.zoom.value);
@ -653,18 +654,13 @@ export const _renderScene = ({
// Paint visible elements // Paint visible elements
const visibleElements = elements.filter((element) => const visibleElements = elements.filter((element) =>
isVisibleElement( isVisibleElement(element, normalizedCanvasWidth, normalizedCanvasHeight, {
element,
normalizedCanvasWidth,
normalizedCanvasHeight,
{
zoom: renderConfig.zoom, zoom: renderConfig.zoom,
offsetLeft: appState.offsetLeft, offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop, offsetTop: appState.offsetTop,
scrollX: renderConfig.scrollX, scrollX: renderConfig.scrollX,
scrollY: renderConfig.scrollY, scrollY: renderConfig.scrollY,
}, }),
),
); );
if (isExporting && exportBackgroundImage) { if (isExporting && exportBackgroundImage) {
@ -707,9 +703,8 @@ export const _renderScene = ({
} }
}); });
let editingLinearElement: let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
| NonDeleted<ExcalidrawLinearElement> undefined;
| undefined = undefined;
visibleElements visibleElements
.filter((el) => !isEmbeddableOrFrameLabel(el)) .filter((el) => !isEmbeddableOrFrameLabel(el))
@ -863,8 +858,7 @@ export const _renderScene = ({
// correct element from visible elements // correct element from visible elements
if ( if (
locallySelectedElements.length === 1 && locallySelectedElements.length === 1 &&
appState.editingLinearElement?.elementId === appState.editingLinearElement?.elementId === locallySelectedElements[0].id
locallySelectedElements[0].id
) { ) {
renderLinearPointHandles( renderLinearPointHandles(
context, context,
@ -1213,36 +1207,30 @@ export const _renderScene = ({
context.restore(); context.restore();
renderOutput = { return {
atLeastOneVisibleElement: visibleElements.length > 0, atLeastOneVisibleElement: visibleElements.length > 0,
scrollBars, scrollBars,
}; };
});
return {
atLeastOneVisibleElement: renderOutput.atLeastOneVisibleElement,
scrollBars: renderOutput.scrollBars ?? undefined,
};
}; };
const renderSceneThrottled = throttleRAF( const renderSceneThrottled = throttleRAF(
(config: { async (config: {
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
appState: AppState; appState: AppState;
scale: number; scale: number;
rc: RoughCanvas; rc: RoughCanvas;
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
renderConfig: RenderConfig; renderConfig: RenderConfig;
callback?: (data: ReturnType<typeof _renderScene>) => void; callback?: (data: Unpromisify<ReturnType<typeof _renderScene>>) => void;
}) => { }) => {
const ret = _renderScene(config); const ret = await _renderScene(config);
config.callback?.(ret); config.callback?.(ret);
}, },
{ trailing: true }, { trailing: true },
); );
/** renderScene throttled to animation framerate */ /** renderScene throttled to animation framerate */
export const renderScene = <T extends boolean = false>( export const renderScene = async <T extends boolean = false>(
config: { config: {
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
appState: AppState; appState: AppState;
@ -1250,19 +1238,25 @@ export const renderScene = <T extends boolean = false>(
rc: RoughCanvas; rc: RoughCanvas;
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
renderConfig: RenderConfig; renderConfig: RenderConfig;
callback?: (data: ReturnType<typeof _renderScene>) => void; callback?: (data: Unpromisify<ReturnType<typeof _renderScene>>) => void;
}, },
/** Whether to throttle rendering. Defaults to false. /** Whether to throttle rendering. Defaults to false.
* When throttling, no value is returned. Use the callback instead. */ * When throttling, no value is returned. Use the callback instead. */
throttle?: T, throttle?: T,
): T extends true ? void : ReturnType<typeof _renderScene> => { ): Promise<
T extends true ? void : Unpromisify<ReturnType<typeof _renderScene>>
> => {
if (throttle) { if (throttle) {
renderSceneThrottled(config); renderSceneThrottled(config);
return undefined as T extends true ? void : ReturnType<typeof _renderScene>; return undefined as T extends true
? void
: Unpromisify<ReturnType<typeof _renderScene>>;
} }
const ret = _renderScene(config); const ret = await _renderScene(config);
config.callback?.(ret); config.callback?.(ret);
return ret as T extends true ? void : ReturnType<typeof _renderScene>; return ret as T extends true
? void
: Unpromisify<ReturnType<typeof _renderScene>>;
}; };
const renderTransformHandles = ( const renderTransformHandles = (

View File

@ -54,7 +54,7 @@ export const exportToCanvas = async (
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements); const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
renderScene({ await renderScene({
elements, elements,
appState, appState,
scale, scale,

View File

@ -27,7 +27,7 @@ export type RenderConfig = {
/** when exporting the behavior is slightly different (e.g. we can't use /** when exporting the behavior is slightly different (e.g. we can't use
CSS filters), and we disable render optimizations for best output */ CSS filters), and we disable render optimizations for best output */
isExporting: boolean; isExporting: boolean;
exportBackgroundImage?: string; exportBackgroundImage?: string | null;
selectionColor?: string; selectionColor?: string;
}; };

View File

@ -54,3 +54,5 @@ export type Assert<T extends true> = T;
export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number) export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)
? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never) ? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)
: never; : never;
export type Unpromisify<T> = T extends Promise<infer U> ? U : T;