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,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;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ export function exportToCanvas(
|
|||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
zoom: 1,
|
||||
},
|
||||
{
|
||||
offsetX: -minX + exportPadding,
|
||||
|
|
|
@ -27,3 +27,4 @@ export {
|
|||
hasText,
|
||||
} from "./comparisons";
|
||||
export { createScene } from "./createScene";
|
||||
export { getZoomOrigin, getZoomTranslation } from "./zoom";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
30
src/scene/zoom.ts
Normal 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)),
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue