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:
Enzo Ferey 2020-02-15 21:03:32 +01:00 committed by GitHub
parent dd2d7e1a88
commit c7ff4c2ed6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 612 additions and 272 deletions

View file

@ -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

View file

@ -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)

View file

@ -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);
}

View file

@ -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,