123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- import { RoughCanvas } from "roughjs/bin/canvas";
- import { RoughSVG } from "roughjs/bin/svg";
- import { FlooredNumber, AppState } from "../types";
- import { ExcalidrawElement } from "../element/types";
- import { getElementAbsoluteCoords, handlerRectangles } from "../element";
- import { roundRect } from "./roundRect";
- import { SceneState } from "../scene/types";
- import {
- getScrollBars,
- SCROLLBAR_COLOR,
- SCROLLBAR_WIDTH,
- } from "../scene/scrollbars";
- import { getZoomTranslation } from "../scene/zoom";
- import { getSelectedElements } from "../scene/selection";
- import { renderElement, renderElementToSvg } from "./renderElement";
- import colors from "../colors";
- function colorForClientId(clientId: string) {
- // Naive way of getting an integer out of the clientId
- const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
- return colors.elementBackground[sum % colors.elementBackground.length];
- }
- export function renderScene(
- elements: readonly ExcalidrawElement[],
- appState: AppState,
- selectionElement: ExcalidrawElement | null,
- rc: RoughCanvas,
- canvas: HTMLCanvasElement,
- sceneState: SceneState,
- // extra options, currently passed by export helper
- {
- 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,
- }: {
- renderScrollbars?: boolean;
- renderSelection?: boolean;
- renderOptimizations?: boolean;
- } = {},
- ) {
- if (!canvas) {
- return { atLeastOneVisibleElement: false };
- }
- const context = canvas.getContext("2d")!;
- // Get initial scale transform as reference for later usage
- const initialContextTransform = context.getTransform();
- // When doing calculations based on canvas width we should used normalized one
- const normalizedCanvasWidth =
- canvas.width / getContextTransformScaleX(initialContextTransform);
- const normalizedCanvasHeight =
- canvas.height / getContextTransformScaleY(initialContextTransform);
- const zoomTranslation = getZoomTranslation(canvas, sceneState.zoom);
- function applyZoom(context: CanvasRenderingContext2D): void {
- context.save();
- // Handle zoom scaling
- context.setTransform(
- getContextTransformScaleX(initialContextTransform) * sceneState.zoom,
- 0,
- 0,
- getContextTransformScaleY(initialContextTransform) * sceneState.zoom,
- getContextTransformTranslateX(context.getTransform()),
- getContextTransformTranslateY(context.getTransform()),
- );
- // Handle zoom translation
- context.setTransform(
- getContextTransformScaleX(context.getTransform()),
- 0,
- 0,
- getContextTransformScaleY(context.getTransform()),
- getContextTransformTranslateX(initialContextTransform) -
- zoomTranslation.x,
- getContextTransformTranslateY(initialContextTransform) -
- zoomTranslation.y,
- );
- }
- function resetZoom(context: CanvasRenderingContext2D): void {
- context.restore();
- }
- // Paint background
- context.save();
- if (typeof sceneState.viewBackgroundColor === "string") {
- const hasTransparence =
- sceneState.viewBackgroundColor === "transparent" ||
- sceneState.viewBackgroundColor.length === 5 ||
- sceneState.viewBackgroundColor.length === 9;
- if (hasTransparence) {
- context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
- }
- context.fillStyle = sceneState.viewBackgroundColor;
- context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
- } else {
- context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
- }
- context.restore();
- // Paint visible elements
- const visibleElements = elements.filter(element =>
- isVisibleElement(
- element,
- normalizedCanvasWidth,
- normalizedCanvasHeight,
- sceneState,
- ),
- );
- applyZoom(context);
- visibleElements.forEach(element => {
- renderElement(element, rc, context, renderOptimizations, sceneState);
- });
- resetZoom(context);
- // Pain selection element
- if (selectionElement) {
- applyZoom(context);
- renderElement(
- selectionElement,
- rc,
- context,
- renderOptimizations,
- sceneState,
- );
- resetZoom(context);
- }
- // Pain selected elements
- if (renderSelection) {
- const selectedElements = getSelectedElements(elements, appState);
- const dashledLinePadding = 4 / sceneState.zoom;
- applyZoom(context);
- context.translate(sceneState.scrollX, sceneState.scrollY);
- selectedElements.forEach(element => {
- const [
- elementX1,
- elementY1,
- elementX2,
- elementY2,
- ] = getElementAbsoluteCoords(element);
- const elementWidth = elementX2 - elementX1;
- const elementHeight = elementY2 - elementY1;
- const initialLineDash = context.getLineDash();
- context.setLineDash([8 / sceneState.zoom, 4 / sceneState.zoom]);
- context.strokeRect(
- elementX1 - dashledLinePadding,
- elementY1 - dashledLinePadding,
- elementWidth + dashledLinePadding * 2,
- elementHeight + dashledLinePadding * 2,
- );
- context.setLineDash(initialLineDash);
- });
- resetZoom(context);
- // Paint resize handlers
- if (selectedElements.length === 1 && selectedElements[0].type !== "text") {
- applyZoom(context);
- context.translate(sceneState.scrollX, sceneState.scrollY);
- context.fillStyle = "#fff";
- const handlers = handlerRectangles(selectedElements[0], sceneState.zoom);
- Object.values(handlers)
- .filter(handler => handler !== undefined)
- .forEach(handler => {
- context.fillRect(handler[0], handler[1], handler[2], handler[3]);
- context.strokeRect(handler[0], handler[1], handler[2], handler[3]);
- });
- resetZoom(context);
- }
- }
- // Paint remote pointers
- for (const clientId in sceneState.remotePointerViewportCoords) {
- const { x, y } = sceneState.remotePointerViewportCoords[clientId];
- const color = colorForClientId(clientId);
- const strokeStyle = context.strokeStyle;
- const fillStyle = context.fillStyle;
- context.strokeStyle = color;
- context.fillStyle = color;
- context.beginPath();
- context.moveTo(x, y);
- context.lineTo(x + 1, y + 14);
- context.lineTo(x + 4, y + 9);
- context.lineTo(x + 9, y + 10);
- context.lineTo(x, y);
- context.fill();
- context.stroke();
- context.strokeStyle = strokeStyle;
- context.fillStyle = fillStyle;
- }
- // Paint scrollbars
- if (renderScrollbars) {
- const scrollBars = getScrollBars(
- elements,
- normalizedCanvasWidth,
- normalizedCanvasHeight,
- sceneState,
- );
- context.save();
- context.fillStyle = SCROLLBAR_COLOR;
- context.strokeStyle = "rgba(255,255,255,0.8)";
- [scrollBars.horizontal, scrollBars.vertical].forEach(scrollBar => {
- if (scrollBar) {
- roundRect(
- context,
- scrollBar.x,
- scrollBar.y,
- scrollBar.width,
- scrollBar.height,
- SCROLLBAR_WIDTH / 2,
- );
- }
- });
- context.restore();
- return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
- }
- return { atLeastOneVisibleElement: visibleElements.length > 0 };
- }
- function isVisibleElement(
- element: ExcalidrawElement,
- viewportWidth: number,
- viewportHeight: number,
- {
- scrollX,
- scrollY,
- zoom,
- }: {
- scrollX: FlooredNumber;
- scrollY: FlooredNumber;
- zoom: number;
- },
- ) {
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
- // Apply zoom
- const viewportWidthWithZoom = viewportWidth / zoom;
- const viewportHeightWithZoom = viewportHeight / zoom;
- const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
- const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
- return (
- x2 + scrollX - viewportWidthDiff / 2 >= 0 &&
- x1 + scrollX - viewportWidthDiff / 2 <= viewportWidthWithZoom &&
- y2 + scrollY - viewportHeightDiff / 2 >= 0 &&
- y1 + scrollY - viewportHeightDiff / 2 <= viewportHeightWithZoom
- );
- }
- // This should be only called for exporting purposes
- export function renderSceneToSvg(
- elements: readonly ExcalidrawElement[],
- rsvg: RoughSVG,
- svgRoot: SVGElement,
- {
- offsetX = 0,
- offsetY = 0,
- }: {
- offsetX?: number;
- offsetY?: number;
- } = {},
- ) {
- if (!svgRoot) {
- return;
- }
- // render elements
- elements.forEach(element => {
- renderElementToSvg(
- element,
- rsvg,
- svgRoot,
- element.x + offsetX,
- element.y + offsetY,
- );
- });
- }
- function getContextTransformScaleX(transform: DOMMatrix): number {
- return transform.a;
- }
- function getContextTransformScaleY(transform: DOMMatrix): number {
- return transform.d;
- }
- function getContextTransformTranslateX(transform: DOMMatrix): number {
- return transform.e;
- }
- function getContextTransformTranslateY(transform: DOMMatrix): number {
- return transform.f;
- }
|