123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845 |
- import { RoughCanvas } from "roughjs/bin/canvas";
- import { RoughSVG } from "roughjs/bin/svg";
- import oc from "open-color";
- import { AppState, Zoom } from "../types";
- import {
- ExcalidrawElement,
- NonDeletedExcalidrawElement,
- ExcalidrawLinearElement,
- NonDeleted,
- GroupId,
- ExcalidrawBindableElement,
- } from "../element/types";
- import {
- getElementAbsoluteCoords,
- OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
- getTransformHandlesFromCoords,
- getTransformHandles,
- getElementBounds,
- getCommonBounds,
- } from "../element";
- import { roundRect } from "./roundRect";
- import { SceneState } from "../scene/types";
- import {
- getScrollBars,
- SCROLLBAR_COLOR,
- SCROLLBAR_WIDTH,
- } from "../scene/scrollbars";
- import { getSelectedElements } from "../scene/selection";
- import { renderElement, renderElementToSvg } from "./renderElement";
- import { getClientColors } from "../clients";
- import { LinearElementEditor } from "../element/linearElementEditor";
- import {
- isSelectedViaGroup,
- getSelectedGroupIds,
- getElementsInGroup,
- } from "../groups";
- import { maxBindingGap } from "../element/collision";
- import {
- SuggestedBinding,
- SuggestedPointBinding,
- isBindingEnabled,
- } from "../element/binding";
- import {
- TransformHandles,
- TransformHandleType,
- } from "../element/transformHandles";
- import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils";
- import { UserIdleState } from "../excalidraw-app/collab/types";
- import { THEME_FILTER } from "../constants";
- const hasEmojiSupport = supportsEmoji();
- const strokeRectWithRotation = (
- context: CanvasRenderingContext2D,
- x: number,
- y: number,
- width: number,
- height: number,
- cx: number,
- cy: number,
- angle: number,
- fill: boolean = false,
- ) => {
- context.translate(cx, cy);
- context.rotate(angle);
- if (fill) {
- context.fillRect(x - cx, y - cy, width, height);
- }
- context.strokeRect(x - cx, y - cy, width, height);
- context.rotate(-angle);
- context.translate(-cx, -cy);
- };
- const strokeDiamondWithRotation = (
- context: CanvasRenderingContext2D,
- width: number,
- height: number,
- cx: number,
- cy: number,
- angle: number,
- ) => {
- context.translate(cx, cy);
- context.rotate(angle);
- context.beginPath();
- context.moveTo(0, height / 2);
- context.lineTo(width / 2, 0);
- context.lineTo(0, -height / 2);
- context.lineTo(-width / 2, 0);
- context.closePath();
- context.stroke();
- context.rotate(-angle);
- context.translate(-cx, -cy);
- };
- const strokeEllipseWithRotation = (
- context: CanvasRenderingContext2D,
- width: number,
- height: number,
- cx: number,
- cy: number,
- angle: number,
- ) => {
- context.beginPath();
- context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
- context.stroke();
- };
- const fillCircle = (
- context: CanvasRenderingContext2D,
- cx: number,
- cy: number,
- radius: number,
- ) => {
- context.beginPath();
- context.arc(cx, cy, radius, 0, Math.PI * 2);
- context.fill();
- context.stroke();
- };
- const strokeGrid = (
- context: CanvasRenderingContext2D,
- gridSize: number,
- offsetX: number,
- offsetY: number,
- width: number,
- height: number,
- ) => {
- const origStrokeStyle = context.strokeStyle;
- context.strokeStyle = "rgba(0,0,0,0.1)";
- context.beginPath();
- for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
- context.moveTo(x, offsetY - gridSize);
- context.lineTo(x, offsetY + height + gridSize * 2);
- }
- for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
- context.moveTo(offsetX - gridSize, y);
- context.lineTo(offsetX + width + gridSize * 2, y);
- }
- context.stroke();
- context.strokeStyle = origStrokeStyle;
- };
- const renderLinearPointHandles = (
- context: CanvasRenderingContext2D,
- appState: AppState,
- sceneState: SceneState,
- element: NonDeleted<ExcalidrawLinearElement>,
- ) => {
- context.translate(sceneState.scrollX, sceneState.scrollY);
- const origStrokeStyle = context.strokeStyle;
- const lineWidth = context.lineWidth;
- context.lineWidth = 1 / sceneState.zoom.value;
- LinearElementEditor.getPointsGlobalCoordinates(element).forEach(
- (point, idx) => {
- context.strokeStyle = "red";
- context.setLineDash([]);
- context.fillStyle =
- appState.editingLinearElement?.activePointIndex === idx
- ? "rgba(255, 127, 127, 0.9)"
- : "rgba(255, 255, 255, 0.9)";
- const { POINT_HANDLE_SIZE } = LinearElementEditor;
- fillCircle(
- context,
- point[0],
- point[1],
- POINT_HANDLE_SIZE / 2 / sceneState.zoom.value,
- );
- },
- );
- context.setLineDash([]);
- context.lineWidth = lineWidth;
- context.translate(-sceneState.scrollX, -sceneState.scrollY);
- context.strokeStyle = origStrokeStyle;
- };
- export const renderScene = (
- elements: readonly NonDeletedExcalidrawElement[],
- appState: AppState,
- selectionElement: NonDeletedExcalidrawElement | null,
- scale: number,
- 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,
- renderGrid = true,
- }: {
- renderScrollbars?: boolean;
- renderSelection?: boolean;
- renderOptimizations?: boolean;
- renderGrid?: boolean;
- } = {},
- ) => {
- if (!canvas) {
- return { atLeastOneVisibleElement: false };
- }
- const context = canvas.getContext("2d")!;
- context.scale(scale, scale);
- // When doing calculations based on canvas width we should used normalized one
- const normalizedCanvasWidth = canvas.width / scale;
- const normalizedCanvasHeight = canvas.height / scale;
- if (sceneState.exportWithDarkMode) {
- context.filter = THEME_FILTER;
- }
- // Paint background
- if (typeof sceneState.viewBackgroundColor === "string") {
- const hasTransparence =
- sceneState.viewBackgroundColor === "transparent" ||
- sceneState.viewBackgroundColor.length === 5 || // #RGBA
- sceneState.viewBackgroundColor.length === 9 || // #RRGGBBA
- /(hsla|rgba)\(/.test(sceneState.viewBackgroundColor);
- if (hasTransparence) {
- context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
- }
- const fillStyle = context.fillStyle;
- context.fillStyle = sceneState.viewBackgroundColor;
- context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
- context.fillStyle = fillStyle;
- } else {
- context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
- }
- // Apply zoom
- const zoomTranslationX = sceneState.zoom.translation.x;
- const zoomTranslationY = sceneState.zoom.translation.y;
- context.translate(zoomTranslationX, zoomTranslationY);
- context.scale(sceneState.zoom.value, sceneState.zoom.value);
- // Grid
- if (renderGrid && appState.gridSize) {
- strokeGrid(
- context,
- appState.gridSize,
- -Math.ceil(zoomTranslationX / sceneState.zoom.value / appState.gridSize) *
- appState.gridSize +
- (sceneState.scrollX % appState.gridSize),
- -Math.ceil(zoomTranslationY / sceneState.zoom.value / appState.gridSize) *
- appState.gridSize +
- (sceneState.scrollY % appState.gridSize),
- normalizedCanvasWidth / sceneState.zoom.value,
- normalizedCanvasHeight / sceneState.zoom.value,
- );
- }
- // Paint visible elements
- const visibleElements = elements.filter((element) =>
- isVisibleElement(element, normalizedCanvasWidth, normalizedCanvasHeight, {
- zoom: sceneState.zoom,
- offsetLeft: appState.offsetLeft,
- offsetTop: appState.offsetTop,
- scrollX: sceneState.scrollX,
- scrollY: sceneState.scrollY,
- }),
- );
- visibleElements.forEach((element) => {
- renderElement(element, rc, context, renderOptimizations, sceneState);
- });
- if (appState.editingLinearElement) {
- const element = LinearElementEditor.getElement(
- appState.editingLinearElement.elementId,
- );
- if (element) {
- renderLinearPointHandles(context, appState, sceneState, element);
- }
- }
- // Paint selection element
- if (selectionElement) {
- renderElement(
- selectionElement,
- rc,
- context,
- renderOptimizations,
- sceneState,
- );
- }
- if (isBindingEnabled(appState)) {
- appState.suggestedBindings
- .filter((binding) => binding != null)
- .forEach((suggestedBinding) => {
- renderBindingHighlight(context, sceneState, suggestedBinding!);
- });
- }
- // Paint selected elements
- if (
- renderSelection &&
- !appState.multiElement &&
- !appState.editingLinearElement
- ) {
- const selections = elements.reduce((acc, element) => {
- const selectionColors = [];
- // local user
- if (
- appState.selectedElementIds[element.id] &&
- !isSelectedViaGroup(appState, element)
- ) {
- selectionColors.push(oc.black);
- }
- // remote users
- if (sceneState.remoteSelectedElementIds[element.id]) {
- selectionColors.push(
- ...sceneState.remoteSelectedElementIds[element.id].map((socketId) => {
- const { background } = getClientColors(socketId, appState);
- return background;
- }),
- );
- }
- if (selectionColors.length) {
- const [
- elementX1,
- elementY1,
- elementX2,
- elementY2,
- ] = getElementAbsoluteCoords(element);
- acc.push({
- angle: element.angle,
- elementX1,
- elementY1,
- elementX2,
- elementY2,
- selectionColors,
- });
- }
- return acc;
- }, [] as { angle: number; elementX1: number; elementY1: number; elementX2: number; elementY2: number; selectionColors: string[] }[]);
- const addSelectionForGroupId = (groupId: GroupId) => {
- const groupElements = getElementsInGroup(elements, groupId);
- const [elementX1, elementY1, elementX2, elementY2] = getCommonBounds(
- groupElements,
- );
- selections.push({
- angle: 0,
- elementX1,
- elementX2,
- elementY1,
- elementY2,
- selectionColors: [oc.black],
- });
- };
- for (const groupId of getSelectedGroupIds(appState)) {
- // TODO: support multiplayer selected group IDs
- addSelectionForGroupId(groupId);
- }
- if (appState.editingGroupId) {
- addSelectionForGroupId(appState.editingGroupId);
- }
- selections.forEach((selection) =>
- renderSelectionBorder(context, sceneState, selection),
- );
- const locallySelectedElements = getSelectedElements(elements, appState);
- // Paint resize transformHandles
- context.translate(sceneState.scrollX, sceneState.scrollY);
- if (locallySelectedElements.length === 1) {
- context.fillStyle = oc.white;
- const transformHandles = getTransformHandles(
- locallySelectedElements[0],
- sceneState.zoom,
- "mouse", // when we render we don't know which pointer type so use mouse
- );
- if (!appState.viewModeEnabled) {
- renderTransformHandles(
- context,
- sceneState,
- transformHandles,
- locallySelectedElements[0].angle,
- );
- }
- } else if (locallySelectedElements.length > 1 && !appState.isRotating) {
- const dashedLinePadding = 4 / sceneState.zoom.value;
- context.fillStyle = oc.white;
- const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements);
- const initialLineDash = context.getLineDash();
- context.setLineDash([2 / sceneState.zoom.value]);
- const lineWidth = context.lineWidth;
- context.lineWidth = 1 / sceneState.zoom.value;
- strokeRectWithRotation(
- context,
- x1 - dashedLinePadding,
- y1 - dashedLinePadding,
- x2 - x1 + dashedLinePadding * 2,
- y2 - y1 + dashedLinePadding * 2,
- (x1 + x2) / 2,
- (y1 + y2) / 2,
- 0,
- );
- context.lineWidth = lineWidth;
- context.setLineDash(initialLineDash);
- const transformHandles = getTransformHandlesFromCoords(
- [x1, y1, x2, y2],
- 0,
- sceneState.zoom,
- "mouse",
- OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
- );
- renderTransformHandles(context, sceneState, transformHandles, 0);
- }
- context.translate(-sceneState.scrollX, -sceneState.scrollY);
- }
- // Reset zoom
- context.scale(1 / sceneState.zoom.value, 1 / sceneState.zoom.value);
- context.translate(-zoomTranslationX, -zoomTranslationY);
- // Paint remote pointers
- for (const clientId in sceneState.remotePointerViewportCoords) {
- let { x, y } = sceneState.remotePointerViewportCoords[clientId];
- x -= appState.offsetLeft;
- y -= appState.offsetTop;
- const width = 9;
- const height = 14;
- const isOutOfBounds =
- x < 0 ||
- x > normalizedCanvasWidth - width ||
- y < 0 ||
- y > normalizedCanvasHeight - height;
- x = Math.max(x, 0);
- x = Math.min(x, normalizedCanvasWidth - width);
- y = Math.max(y, 0);
- y = Math.min(y, normalizedCanvasHeight - height);
- const { background, stroke } = getClientColors(clientId, appState);
- const strokeStyle = context.strokeStyle;
- const fillStyle = context.fillStyle;
- const globalAlpha = context.globalAlpha;
- context.strokeStyle = stroke;
- context.fillStyle = background;
- const userState = sceneState.remotePointerUserStates[clientId];
- if (isOutOfBounds || userState === UserIdleState.AWAY) {
- context.globalAlpha = 0.48;
- }
- if (
- sceneState.remotePointerButton &&
- sceneState.remotePointerButton[clientId] === "down"
- ) {
- context.beginPath();
- context.arc(x, y, 15, 0, 2 * Math.PI, false);
- context.lineWidth = 3;
- context.strokeStyle = "#ffffff88";
- context.stroke();
- context.closePath();
- context.beginPath();
- context.arc(x, y, 15, 0, 2 * Math.PI, false);
- context.lineWidth = 1;
- context.strokeStyle = stroke;
- context.stroke();
- context.closePath();
- }
- 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();
- const username = sceneState.remotePointerUsernames[clientId];
- let idleState = "";
- if (userState === UserIdleState.AWAY) {
- idleState = hasEmojiSupport ? "⚫️" : ` (${UserIdleState.AWAY})`;
- } else if (userState === UserIdleState.IDLE) {
- idleState = hasEmojiSupport ? "💤" : ` (${UserIdleState.IDLE})`;
- } else if (userState === UserIdleState.ACTIVE) {
- idleState = hasEmojiSupport ? "🟢" : "";
- }
- const usernameAndIdleState = `${
- username ? `${username} ` : ""
- }${idleState}`;
- if (!isOutOfBounds && usernameAndIdleState) {
- const offsetX = x + width;
- const offsetY = y + height;
- const paddingHorizontal = 4;
- const paddingVertical = 4;
- const measure = context.measureText(usernameAndIdleState);
- const measureHeight =
- measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
- // Border
- context.fillStyle = stroke;
- context.fillRect(
- offsetX - 1,
- offsetY - 1,
- measure.width + 2 * paddingHorizontal + 2,
- measureHeight + 2 * paddingVertical + 2,
- );
- // Background
- context.fillStyle = background;
- context.fillRect(
- offsetX,
- offsetY,
- measure.width + 2 * paddingHorizontal,
- measureHeight + 2 * paddingVertical,
- );
- context.fillStyle = oc.white;
- context.fillText(
- usernameAndIdleState,
- offsetX + paddingHorizontal,
- offsetY + paddingVertical + measure.actualBoundingBoxAscent,
- );
- }
- context.strokeStyle = strokeStyle;
- context.fillStyle = fillStyle;
- context.globalAlpha = globalAlpha;
- context.closePath();
- }
- // Paint scrollbars
- let scrollBars;
- if (renderScrollbars) {
- scrollBars = getScrollBars(
- elements,
- normalizedCanvasWidth,
- normalizedCanvasHeight,
- sceneState,
- );
- const fillStyle = context.fillStyle;
- const strokeStyle = context.strokeStyle;
- 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.fillStyle = fillStyle;
- context.strokeStyle = strokeStyle;
- }
- context.scale(1 / scale, 1 / scale);
- return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
- };
- const renderTransformHandles = (
- context: CanvasRenderingContext2D,
- sceneState: SceneState,
- transformHandles: TransformHandles,
- angle: number,
- ): void => {
- Object.keys(transformHandles).forEach((key) => {
- const transformHandle = transformHandles[key as TransformHandleType];
- if (transformHandle !== undefined) {
- const lineWidth = context.lineWidth;
- context.lineWidth = 1 / sceneState.zoom.value;
- if (key === "rotation") {
- fillCircle(
- context,
- transformHandle[0] + transformHandle[2] / 2,
- transformHandle[1] + transformHandle[3] / 2,
- transformHandle[2] / 2,
- );
- } else {
- strokeRectWithRotation(
- context,
- transformHandle[0],
- transformHandle[1],
- transformHandle[2],
- transformHandle[3],
- transformHandle[0] + transformHandle[2] / 2,
- transformHandle[1] + transformHandle[3] / 2,
- angle,
- true, // fill before stroke
- );
- }
- context.lineWidth = lineWidth;
- }
- });
- };
- const renderSelectionBorder = (
- context: CanvasRenderingContext2D,
- sceneState: SceneState,
- elementProperties: {
- angle: number;
- elementX1: number;
- elementY1: number;
- elementX2: number;
- elementY2: number;
- selectionColors: string[];
- },
- ) => {
- const {
- angle,
- elementX1,
- elementY1,
- elementX2,
- elementY2,
- selectionColors,
- } = elementProperties;
- const elementWidth = elementX2 - elementX1;
- const elementHeight = elementY2 - elementY1;
- const initialLineDash = context.getLineDash();
- const lineWidth = context.lineWidth;
- const lineDashOffset = context.lineDashOffset;
- const strokeStyle = context.strokeStyle;
- const dashedLinePadding = 4 / sceneState.zoom.value;
- const dashWidth = 8 / sceneState.zoom.value;
- const spaceWidth = 4 / sceneState.zoom.value;
- context.lineWidth = 1 / sceneState.zoom.value;
- context.translate(sceneState.scrollX, sceneState.scrollY);
- const count = selectionColors.length;
- for (let index = 0; index < count; ++index) {
- context.strokeStyle = selectionColors[index];
- context.setLineDash([
- dashWidth,
- spaceWidth + (dashWidth + spaceWidth) * (count - 1),
- ]);
- context.lineDashOffset = (dashWidth + spaceWidth) * index;
- strokeRectWithRotation(
- context,
- elementX1 - dashedLinePadding,
- elementY1 - dashedLinePadding,
- elementWidth + dashedLinePadding * 2,
- elementHeight + dashedLinePadding * 2,
- elementX1 + elementWidth / 2,
- elementY1 + elementHeight / 2,
- angle,
- );
- }
- context.lineDashOffset = lineDashOffset;
- context.strokeStyle = strokeStyle;
- context.lineWidth = lineWidth;
- context.setLineDash(initialLineDash);
- context.translate(-sceneState.scrollX, -sceneState.scrollY);
- };
- const renderBindingHighlight = (
- context: CanvasRenderingContext2D,
- sceneState: SceneState,
- suggestedBinding: SuggestedBinding,
- ) => {
- // preserve context settings to restore later
- const originalStrokeStyle = context.strokeStyle;
- const originalLineWidth = context.lineWidth;
- const renderHighlight = Array.isArray(suggestedBinding)
- ? renderBindingHighlightForSuggestedPointBinding
- : renderBindingHighlightForBindableElement;
- context.translate(sceneState.scrollX, sceneState.scrollY);
- renderHighlight(context, suggestedBinding as any);
- // restore context settings
- context.strokeStyle = originalStrokeStyle;
- context.lineWidth = originalLineWidth;
- context.translate(-sceneState.scrollX, -sceneState.scrollY);
- };
- const renderBindingHighlightForBindableElement = (
- context: CanvasRenderingContext2D,
- element: ExcalidrawBindableElement,
- ) => {
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
- const width = x2 - x1;
- const height = y2 - y1;
- const threshold = maxBindingGap(element, width, height);
- // So that we don't overlap the element itself
- const strokeOffset = 4;
- context.strokeStyle = "rgba(0,0,0,.05)";
- context.lineWidth = threshold - strokeOffset;
- const padding = strokeOffset / 2 + threshold / 2;
- switch (element.type) {
- case "rectangle":
- case "text":
- strokeRectWithRotation(
- context,
- x1 - padding,
- y1 - padding,
- width + padding * 2,
- height + padding * 2,
- x1 + width / 2,
- y1 + height / 2,
- element.angle,
- );
- break;
- case "diamond":
- const side = Math.hypot(width, height);
- const wPadding = (padding * side) / height;
- const hPadding = (padding * side) / width;
- strokeDiamondWithRotation(
- context,
- width + wPadding * 2,
- height + hPadding * 2,
- x1 + width / 2,
- y1 + height / 2,
- element.angle,
- );
- break;
- case "ellipse":
- strokeEllipseWithRotation(
- context,
- width + padding * 2,
- height + padding * 2,
- x1 + width / 2,
- y1 + height / 2,
- element.angle,
- );
- break;
- }
- };
- const renderBindingHighlightForSuggestedPointBinding = (
- context: CanvasRenderingContext2D,
- suggestedBinding: SuggestedPointBinding,
- ) => {
- const [element, startOrEnd, bindableElement] = suggestedBinding;
- const threshold = maxBindingGap(
- bindableElement,
- bindableElement.width,
- bindableElement.height,
- );
- context.strokeStyle = "rgba(0,0,0,0)";
- context.fillStyle = "rgba(0,0,0,.05)";
- const pointIndices =
- startOrEnd === "both" ? [0, -1] : startOrEnd === "start" ? [0] : [-1];
- pointIndices.forEach((index) => {
- const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
- element,
- index,
- );
- fillCircle(context, x, y, threshold);
- });
- };
- const isVisibleElement = (
- element: ExcalidrawElement,
- canvasWidth: number,
- canvasHeight: number,
- viewTransformations: {
- zoom: Zoom;
- offsetLeft: number;
- offsetTop: number;
- scrollX: number;
- scrollY: number;
- },
- ) => {
- const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates
- const topLeftSceneCoords = viewportCoordsToSceneCoords(
- {
- clientX: viewTransformations.offsetLeft,
- clientY: viewTransformations.offsetTop,
- },
- viewTransformations,
- );
- const bottomRightSceneCoords = viewportCoordsToSceneCoords(
- {
- clientX: viewTransformations.offsetLeft + canvasWidth,
- clientY: viewTransformations.offsetTop + canvasHeight,
- },
- viewTransformations,
- );
- return (
- topLeftSceneCoords.x <= x2 &&
- topLeftSceneCoords.y <= y2 &&
- bottomRightSceneCoords.x >= x1 &&
- bottomRightSceneCoords.y >= y1
- );
- };
- // This should be only called for exporting purposes
- export const renderSceneToSvg = (
- elements: readonly NonDeletedExcalidrawElement[],
- rsvg: RoughSVG,
- svgRoot: SVGElement,
- {
- offsetX = 0,
- offsetY = 0,
- }: {
- offsetX?: number;
- offsetY?: number;
- } = {},
- ) => {
- if (!svgRoot) {
- return;
- }
- // render elements
- elements.forEach((element) => {
- if (!element.isDeleted) {
- renderElementToSvg(
- element,
- rsvg,
- svgRoot,
- element.x + offsetX,
- element.y + offsetY,
- );
- }
- });
- };
|