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,6 @@
import { ExcalidrawElement } from "../element/types";
import { hitTest } from "../element/collision";
import { getElementAbsoluteCoords } from "../element";
import { getElementAbsoluteCoords, hitTest } from "../element";
export const hasBackground = (type: string) =>
type === "rectangle" || type === "ellipse" || type === "diamond";
@ -18,11 +18,12 @@ export function getElementAtPosition(
elements: readonly ExcalidrawElement[],
x: number,
y: number,
zoom: number,
) {
let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array)
for (let i = elements.length - 1; i >= 0; --i) {
if (hitTest(elements[i], x, y)) {
if (hitTest(elements[i], x, y, zoom)) {
hitElement = elements[i];
break;
}

View file

@ -44,6 +44,7 @@ export function exportToCanvas(
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: 0,
scrollY: 0,
zoom: 1,
},
{
offsetX: -minX + exportPadding,

View file

@ -27,3 +27,4 @@ export {
hasText,
} from "./comparisons";
export { createScene } from "./createScene";
export { getZoomOrigin, getZoomTranslation } from "./zoom";

View file

@ -9,8 +9,15 @@ export function getScrollBars(
elements: readonly ExcalidrawElement[],
viewportWidth: number,
viewportHeight: number,
scrollX: number,
scrollY: number,
{
scrollX,
scrollY,
zoom,
}: {
scrollX: number;
scrollY: number;
zoom: number;
},
) {
// This is the bounding box of all the elements
const [
@ -20,11 +27,18 @@ export function getScrollBars(
elementsMaxY,
] = getCommonBounds(elements);
// Apply zoom
const viewportWidthWithZoom = viewportWidth / zoom;
const viewportHeightWithZoom = viewportHeight / zoom;
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
// The viewport is the rectangle currently visible for the user
const viewportMinX = -scrollX;
const viewportMinY = -scrollY;
const viewportMaxX = -scrollX + viewportWidth;
const viewportMaxY = -scrollY + viewportHeight;
const viewportMinX = -scrollX + viewportWidthDiff / 2;
const viewportMinY = -scrollY + viewportHeightDiff / 2;
const viewportMaxX = viewportMinX + viewportWidthWithZoom;
const viewportMaxY = viewportMinY + viewportHeightWithZoom;
// The scene is the bounding box of both the elements and viewport
const sceneMinX = Math.min(elementsMinX, viewportMinX);
@ -74,16 +88,21 @@ export function isOverScrollBars(
y: number,
viewportWidth: number,
viewportHeight: number,
scrollX: number,
scrollY: number,
) {
const scrollBars = getScrollBars(
elements,
viewportWidth,
viewportHeight,
{
scrollX,
scrollY,
);
zoom,
}: {
scrollX: number;
scrollY: number;
zoom: number;
},
) {
const scrollBars = getScrollBars(elements, viewportWidth, viewportHeight, {
scrollX,
scrollY,
zoom,
});
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
scrollBars.horizontal,

View file

@ -5,6 +5,7 @@ export type SceneState = {
scrollY: number;
// null indicates transparent bg
viewBackgroundColor: string | null;
zoom: number;
};
export type SceneScroll = {

30
src/scene/zoom.ts Normal file
View file

@ -0,0 +1,30 @@
export function getZoomOrigin(canvas: HTMLCanvasElement | null) {
if (canvas === null) {
return { x: 0, y: 0 };
}
const context = canvas.getContext("2d");
if (context === null) {
return { x: 0, y: 0 };
}
const normalizedCanvasWidth = canvas.width / context.getTransform().a;
const normalizedCanvasHeight = canvas.height / context.getTransform().d;
return {
x: normalizedCanvasWidth / 2,
y: normalizedCanvasHeight / 2,
};
}
export function getZoomTranslation(canvas: HTMLCanvasElement, zoom: number) {
const diffMiddleOfTheCanvas = {
x: (canvas.width / 2) * (zoom - 1),
y: (canvas.height / 2) * (zoom - 1),
};
// Due to JavaScript float precision, we fix to fix decimals count to have symmetric zoom
return {
x: parseFloat(diffMiddleOfTheCanvas.x.toFixed(8)),
y: parseFloat(diffMiddleOfTheCanvas.y.toFixed(8)),
};
}