Fast & Furious (#655)

* [WIP] Fast & Furious

* ensure we translate before scaling

* implement canvas caching for rest of elements

* remove unnecessary ts-ignore

* fix for devicePixelRatio

* initialize missing element props on restore

* factor out canvas padding

* remove unnecessary filtering

* simplify renderElement

* regenerate canvas on prop changes

* revert swapping shape resetting with canvas

* fix blurry rendering

* apply devicePixelRatio when clearing canvas

* improve blurriness; fix arrow canvas offset

* revert canvas clearing changes in anticipation of merge

* normalize scrollX/Y on update

* fix getDerivedStateFromProps

* swap derivedState for type brands

* tweak types

* remove renderScene offsets

* move selection element translations to renderElement

* dry out canvas zoom transformations

* fix padding offset

* Render cached canvas based on the zoom level

Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: Preet <833927+pshihn@users.noreply.github.com>
This commit is contained in:
Christopher Chedeau 2020-02-19 08:25:01 -08:00 committed by GitHub
parent d39c7d4421
commit 5256096d76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 269 additions and 114 deletions

View file

@ -1,6 +1,7 @@
import { RoughCanvas } from "roughjs/bin/canvas";
import { RoughSVG } from "roughjs/bin/svg";
import { FlooredNumber } from "../types";
import { ExcalidrawElement } from "../element/types";
import { getElementAbsoluteCoords, handlerRectangles } from "../element";
@ -24,28 +25,22 @@ export function renderScene(
sceneState: SceneState,
// extra options, currently passed by export helper
{
offsetX,
offsetY,
renderScrollbars = true,
renderSelection = true,
// Whether to employ render optimizations to improve performance.
// Should not be turned on for export operations and similar, because it
// doesn't guarantee pixel-perfect output.
renderOptimizations = false,
}: {
offsetX?: number;
offsetY?: number;
renderScrollbars?: boolean;
renderSelection?: boolean;
renderOptimizations?: boolean;
} = {},
): boolean {
if (!canvas) {
return false;
}
// Use offsets insteads of scrolls if available
sceneState = {
...sceneState,
scrollX: typeof offsetX === "number" ? offsetX : sceneState.scrollX,
scrollY: typeof offsetY === "number" ? offsetY : sceneState.scrollY,
};
const context = canvas.getContext("2d")!;
// Get initial scale transform as reference for later usage
@ -57,8 +52,11 @@ export function renderScene(
const normalizedCanvasHeight =
canvas.height / getContextTransformScaleY(initialContextTransform);
// Handle zoom scaling
function scaleContextToZoom() {
const zoomTranslation = getZoomTranslation(canvas, sceneState.zoom);
function applyZoom(context: CanvasRenderingContext2D): void {
context.save();
// Handle zoom scaling
context.setTransform(
getContextTransformScaleX(initialContextTransform) * sceneState.zoom,
0,
@ -67,11 +65,7 @@ export function renderScene(
getContextTransformTranslateX(context.getTransform()),
getContextTransformTranslateY(context.getTransform()),
);
}
// Handle zoom translation
const zoomTranslation = getZoomTranslation(canvas, sceneState.zoom);
function translateContextToZoom() {
// Handle zoom translation
context.setTransform(
getContextTransformScaleX(context.getTransform()),
0,
@ -83,6 +77,9 @@ export function renderScene(
zoomTranslation.y,
);
}
function resetZoom(context: CanvasRenderingContext2D): void {
context.restore();
}
// Paint background
context.save();
@ -111,27 +108,23 @@ export function renderScene(
),
);
context.save();
scaleContextToZoom();
translateContextToZoom();
context.translate(sceneState.scrollX, sceneState.scrollY);
applyZoom(context);
visibleElements.forEach(element => {
context.save();
context.translate(element.x, element.y);
renderElement(element, rc, context);
context.restore();
renderElement(element, rc, context, renderOptimizations, sceneState);
});
context.restore();
resetZoom(context);
// Pain selection element
if (selectionElement) {
context.save();
scaleContextToZoom();
translateContextToZoom();
context.translate(sceneState.scrollX, sceneState.scrollY);
context.translate(selectionElement.x, selectionElement.y);
renderElement(selectionElement, rc, context);
context.restore();
applyZoom(context);
renderElement(
selectionElement,
rc,
context,
renderOptimizations,
sceneState,
);
resetZoom(context);
}
// Pain selected elements
@ -139,9 +132,7 @@ export function renderScene(
const selectedElements = getSelectedElements(elements);
const dashledLinePadding = 4 / sceneState.zoom;
context.save();
scaleContextToZoom();
translateContextToZoom();
applyZoom(context);
context.translate(sceneState.scrollX, sceneState.scrollY);
selectedElements.forEach(element => {
const [
@ -164,13 +155,11 @@ export function renderScene(
);
context.setLineDash(initialLineDash);
});
context.restore();
resetZoom(context);
// Paint resize handlers
if (selectedElements.length === 1 && selectedElements[0].type !== "text") {
context.save();
scaleContextToZoom();
translateContextToZoom();
applyZoom(context);
context.translate(sceneState.scrollX, sceneState.scrollY);
const handlers = handlerRectangles(selectedElements[0], sceneState.zoom);
Object.values(handlers)
@ -178,8 +167,10 @@ export function renderScene(
.forEach(handler => {
context.strokeRect(handler[0], handler[1], handler[2], handler[3]);
});
context.restore();
resetZoom(context);
}
return visibleElements.length > 0;
}
// Paint scrollbars
@ -221,8 +212,8 @@ function isVisibleElement(
scrollY,
zoom,
}: {
scrollX: number;
scrollY: number;
scrollX: FlooredNumber;
scrollY: FlooredNumber;
zoom: number;
},
) {