mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Canvas zooming (#716)
* Zoom icons. * Actions. * Min zoom of 0 does not make sense. * Zoom logic. * Modify how zoom affects selection rendering. * More precise scrollbar dimensions. * Adjust elements visibility and scrollbars. * Normalized canvas width and height. * Apply zoom to resize test. * [WIP] Zoom using canvas center as an origin. * Undo zoom on `getScrollBars`. * WIP: center zoom origin via scroll * This was wrong for sure. * Finish scaling using center as origin. * Almost there. * Scroll offset should be not part of zoom transforms. * Better naming. * Wheel movement should be the same no matter the zoom level. * Panning movement should be the same no matter the zoom level. * Fix elements pasting. * Fix text WYSIWGT. * Fix scrollbars and visibility.
This commit is contained in:
parent
dd2d7e1a88
commit
c7ff4c2ed6
19 changed files with 612 additions and 272 deletions
|
@ -1,6 +1,7 @@
|
|||
import { distanceBetweenPointAndSegment } from "../math";
|
||||
|
||||
import { ExcalidrawElement } from "./types";
|
||||
|
||||
import {
|
||||
getDiamondPoints,
|
||||
getElementAbsoluteCoords,
|
||||
|
@ -17,10 +18,11 @@ export function hitTest(
|
|||
element: ExcalidrawElement,
|
||||
x: number,
|
||||
y: number,
|
||||
zoom: number,
|
||||
): boolean {
|
||||
// For shapes that are composed of lines, we only enable point-selection when the distance
|
||||
// of the click is less than x pixels of any of the lines that the shape is composed of
|
||||
const lineThreshold = 10;
|
||||
const lineThreshold = 10 / zoom;
|
||||
|
||||
if (element.type === "ellipse") {
|
||||
// https://stackoverflow.com/a/46007540/232122
|
||||
|
|
|
@ -1,98 +1,83 @@
|
|||
import { ExcalidrawElement } from "./types";
|
||||
import { SceneScroll } from "../scene/types";
|
||||
import { getLinearElementAbsoluteBounds } from "./bounds";
|
||||
|
||||
import { getElementAbsoluteCoords } from "./bounds";
|
||||
|
||||
type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se";
|
||||
|
||||
export function handlerRectangles(
|
||||
element: ExcalidrawElement,
|
||||
{ scrollX, scrollY }: SceneScroll,
|
||||
) {
|
||||
let elementX2 = 0;
|
||||
let elementY2 = 0;
|
||||
let elementX1 = Infinity;
|
||||
let elementY1 = Infinity;
|
||||
let marginX = -8;
|
||||
let marginY = -8;
|
||||
export function handlerRectangles(element: ExcalidrawElement, zoom: number) {
|
||||
const handlerWidth = 8 / zoom;
|
||||
const handlerHeight = 8 / zoom;
|
||||
|
||||
const minimumSize = 40;
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
[
|
||||
elementX1,
|
||||
elementY1,
|
||||
elementX2,
|
||||
elementY2,
|
||||
] = getLinearElementAbsoluteBounds(element);
|
||||
} else {
|
||||
elementX1 = element.x;
|
||||
elementX2 = element.x + element.width;
|
||||
elementY1 = element.y;
|
||||
elementY2 = element.y + element.height;
|
||||
const handlerMarginX = 8 / zoom;
|
||||
const handlerMarginY = 8 / zoom;
|
||||
|
||||
marginX = element.width < 0 ? 8 : -8;
|
||||
marginY = element.height < 0 ? 8 : -8;
|
||||
}
|
||||
const [elementX1, elementY1, elementX2, elementY2] = getElementAbsoluteCoords(
|
||||
element,
|
||||
);
|
||||
|
||||
const margin = 4;
|
||||
const handlers = {} as { [T in Sides]: number[] };
|
||||
const elementWidth = elementX2 - elementX1;
|
||||
const elementHeight = elementY2 - elementY1;
|
||||
|
||||
if (Math.abs(elementX2 - elementX1) > minimumSize) {
|
||||
const dashedLineMargin = 4 / zoom;
|
||||
|
||||
const handlers = {
|
||||
nw: [
|
||||
elementX1 - dashedLineMargin - handlerMarginX,
|
||||
elementY1 - dashedLineMargin - handlerMarginY,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
ne: [
|
||||
elementX2 + dashedLineMargin,
|
||||
elementY1 - dashedLineMargin - handlerMarginY,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
sw: [
|
||||
elementX1 - dashedLineMargin - handlerMarginX,
|
||||
elementY2 + dashedLineMargin,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
se: [
|
||||
elementX2 + dashedLineMargin,
|
||||
elementY2 + dashedLineMargin,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
],
|
||||
} as { [T in Sides]: number[] };
|
||||
|
||||
// We only want to show height handlers (all cardinal directions) above a certain size
|
||||
const minimumSizeForEightHandlers = 40 / zoom;
|
||||
if (Math.abs(elementWidth) > minimumSizeForEightHandlers) {
|
||||
handlers["n"] = [
|
||||
elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4,
|
||||
elementY1 - margin + scrollY + marginY,
|
||||
8,
|
||||
8,
|
||||
elementX1 + elementWidth / 2,
|
||||
elementY1 - dashedLineMargin - handlerMarginY,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
|
||||
handlers["s"] = [
|
||||
elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4,
|
||||
elementY2 - margin + scrollY - marginY,
|
||||
8,
|
||||
8,
|
||||
elementX1 + elementWidth / 2,
|
||||
elementY2 + dashedLineMargin,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
}
|
||||
|
||||
if (Math.abs(elementY2 - elementY1) > minimumSize) {
|
||||
if (Math.abs(elementHeight) > minimumSizeForEightHandlers) {
|
||||
handlers["w"] = [
|
||||
elementX1 - margin + scrollX + marginX,
|
||||
elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4,
|
||||
8,
|
||||
8,
|
||||
elementX1 - dashedLineMargin - handlerMarginX,
|
||||
elementY1 + elementHeight / 2,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
|
||||
handlers["e"] = [
|
||||
elementX2 - margin + scrollX - marginX,
|
||||
elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4,
|
||||
8,
|
||||
8,
|
||||
elementX2 + dashedLineMargin,
|
||||
elementY1 + elementHeight / 2,
|
||||
handlerWidth,
|
||||
handlerHeight,
|
||||
];
|
||||
}
|
||||
|
||||
handlers["nw"] = [
|
||||
elementX1 - margin + scrollX + marginX,
|
||||
elementY1 - margin + scrollY + marginY,
|
||||
8,
|
||||
8,
|
||||
]; // nw
|
||||
handlers["ne"] = [
|
||||
elementX2 - margin + scrollX - marginX,
|
||||
elementY1 - margin + scrollY + marginY,
|
||||
8,
|
||||
8,
|
||||
]; // ne
|
||||
handlers["sw"] = [
|
||||
elementX1 - margin + scrollX + marginX,
|
||||
elementY2 - margin + scrollY - marginY,
|
||||
8,
|
||||
8,
|
||||
]; // sw
|
||||
handlers["se"] = [
|
||||
elementX2 - margin + scrollX - marginX,
|
||||
elementY2 - margin + scrollY - marginY,
|
||||
8,
|
||||
8,
|
||||
]; // se
|
||||
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
if (element.points.length === 2) {
|
||||
// only check the last point because starting point is always (0,0)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { ExcalidrawElement } from "./types";
|
||||
|
||||
import { handlerRectangles } from "./handlerRectangles";
|
||||
import { SceneScroll } from "../scene/types";
|
||||
|
||||
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
||||
|
||||
|
@ -9,13 +8,13 @@ export function resizeTest(
|
|||
element: ExcalidrawElement,
|
||||
x: number,
|
||||
y: number,
|
||||
{ scrollX, scrollY }: SceneScroll,
|
||||
zoom: number,
|
||||
): HandlerRectanglesRet | false {
|
||||
if (!element.isSelected || element.type === "text") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const handlers = handlerRectangles(element, { scrollX, scrollY });
|
||||
const handlers = handlerRectangles(element, zoom);
|
||||
|
||||
const filter = Object.keys(handlers).filter(key => {
|
||||
const handler = handlers[key as HandlerRectanglesRet]!;
|
||||
|
@ -24,10 +23,10 @@ export function resizeTest(
|
|||
}
|
||||
|
||||
return (
|
||||
x + scrollX >= handler[0] &&
|
||||
x + scrollX <= handler[0] + handler[2] &&
|
||||
y + scrollY >= handler[1] &&
|
||||
y + scrollY <= handler[1] + handler[3]
|
||||
x >= handler[0] &&
|
||||
x <= handler[0] + handler[2] &&
|
||||
y >= handler[1] &&
|
||||
y <= handler[1] + handler[3]
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -41,16 +40,13 @@ export function resizeTest(
|
|||
export function getElementWithResizeHandler(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
{ x, y }: { x: number; y: number },
|
||||
{ scrollX, scrollY }: SceneScroll,
|
||||
zoom: number,
|
||||
) {
|
||||
return elements.reduce((result, element) => {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const resizeHandle = resizeTest(element, x, y, {
|
||||
scrollX,
|
||||
scrollY,
|
||||
});
|
||||
const resizeHandle = resizeTest(element, x, y, zoom);
|
||||
return resizeHandle ? { element, resizeHandle } : null;
|
||||
}, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ type TextWysiwygParams = {
|
|||
strokeColor: string;
|
||||
font: string;
|
||||
opacity: number;
|
||||
zoom: number;
|
||||
onSubmit: (text: string) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
@ -25,6 +26,7 @@ export function textWysiwyg({
|
|||
strokeColor,
|
||||
font,
|
||||
opacity,
|
||||
zoom,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: TextWysiwygParams) {
|
||||
|
@ -43,7 +45,7 @@ export function textWysiwyg({
|
|||
opacity: opacity / 100,
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
transform: "translate(-50%, -50%)",
|
||||
transform: `translate(-50%, -50%) scale(${zoom})`,
|
||||
textAlign: "left",
|
||||
display: "inline-block",
|
||||
font: font,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue