diff --git a/src/components/App.tsx b/src/components/App.tsx index 11e6e6fb..07401c39 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2246,6 +2246,7 @@ export class App extends React.Component { elements, this.state, this.state.selectionElement, + window.devicePixelRatio, this.rc!, this.canvas!, { diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 3aad2fb0..53ff104f 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -303,6 +303,10 @@ export function renderElement( context.fillStyle = "rgba(0, 0, 255, 0.10)"; context.fillRect(0, 0, element.width, element.height); context.fillStyle = fillStyle; + context.translate( + -element.x - sceneState.scrollX, + -element.y - sceneState.scrollY, + ); break; } case "rectangle": diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 95e5305e..46c417ff 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -12,7 +12,6 @@ import { SCROLLBAR_COLOR, SCROLLBAR_WIDTH, } from "../scene/scrollbars"; -import { getZoomTranslation } from "../scene/zoom"; import { getSelectedElements } from "../scene/selection"; import { renderElement, renderElementToSvg } from "./renderElement"; @@ -28,6 +27,7 @@ export function renderScene( elements: readonly ExcalidrawElement[], appState: AppState, selectionElement: ExcalidrawElement | null, + scale: number, rc: RoughCanvas, canvas: HTMLCanvasElement, sceneState: SceneState, @@ -51,46 +51,11 @@ export function renderScene( const context = canvas.getContext("2d")!; - // Get initial scale transform as reference for later usage - const initialContextTransform = context.getTransform(); - // When doing calculations based on canvas width we should used normalized one - const normalizedCanvasWidth = - canvas.width / getContextTransformScaleX(initialContextTransform); - const normalizedCanvasHeight = - canvas.height / getContextTransformScaleY(initialContextTransform); - - const zoomTranslation = getZoomTranslation(canvas, sceneState.zoom); - function applyZoom(context: CanvasRenderingContext2D): void { - context.save(); - - // Handle zoom scaling - context.setTransform( - getContextTransformScaleX(initialContextTransform) * sceneState.zoom, - 0, - 0, - getContextTransformScaleY(initialContextTransform) * sceneState.zoom, - getContextTransformTranslateX(context.getTransform()), - getContextTransformTranslateY(context.getTransform()), - ); - // Handle zoom translation - context.setTransform( - getContextTransformScaleX(context.getTransform()), - 0, - 0, - getContextTransformScaleY(context.getTransform()), - getContextTransformTranslateX(initialContextTransform) - - zoomTranslation.x, - getContextTransformTranslateY(initialContextTransform) - - zoomTranslation.y, - ); - } - function resetZoom(context: CanvasRenderingContext2D): void { - context.restore(); - } + const normalizedCanvasWidth = canvas.width / scale; + const normalizedCanvasHeight = canvas.height / scale; // Paint background - context.save(); if (typeof sceneState.viewBackgroundColor === "string") { const hasTransparence = sceneState.viewBackgroundColor === "transparent" || @@ -99,12 +64,20 @@ export function renderScene( 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); } - context.restore(); + + // Apply zoom + const zoomTranslationX = (-normalizedCanvasWidth * (sceneState.zoom - 1)) / 2; + const zoomTranslationY = + (-normalizedCanvasHeight * (sceneState.zoom - 1)) / 2; + context.translate(zoomTranslationX, zoomTranslationY); + context.scale(sceneState.zoom, sceneState.zoom); // Paint visible elements const visibleElements = elements.filter(element => @@ -116,15 +89,12 @@ export function renderScene( ), ); - applyZoom(context); visibleElements.forEach(element => { renderElement(element, rc, context, renderOptimizations, sceneState); }); - resetZoom(context); // Pain selection element if (selectionElement) { - applyZoom(context); renderElement( selectionElement, rc, @@ -132,7 +102,6 @@ export function renderScene( renderOptimizations, sceneState, ); - resetZoom(context); } // Pain selected elements @@ -140,7 +109,6 @@ export function renderScene( const selectedElements = getSelectedElements(elements, appState); const dashledLinePadding = 4 / sceneState.zoom; - applyZoom(context); context.translate(sceneState.scrollX, sceneState.scrollY); selectedElements.forEach(element => { const [ @@ -166,11 +134,10 @@ export function renderScene( context.lineWidth = lineWidth; context.setLineDash(initialLineDash); }); - resetZoom(context); + context.translate(-sceneState.scrollX, -sceneState.scrollY); // Paint resize handlers if (selectedElements.length === 1 && selectedElements[0].type !== "text") { - applyZoom(context); context.translate(sceneState.scrollX, sceneState.scrollY); context.fillStyle = "#fff"; const handlers = handlerRectangles(selectedElements[0], sceneState.zoom); @@ -183,10 +150,14 @@ export function renderScene( context.strokeRect(handler[0], handler[1], handler[2], handler[3]); context.lineWidth = lineWidth; }); - resetZoom(context); + context.translate(-sceneState.scrollX, -sceneState.scrollY); } } + // Reset zoom + context.scale(1 / sceneState.zoom, 1 / sceneState.zoom); + context.translate(-zoomTranslationX, -zoomTranslationY); + // Paint remote pointers for (const clientId in sceneState.remotePointerViewportCoords) { let { x, y } = sceneState.remotePointerViewportCoords[clientId]; @@ -237,7 +208,8 @@ export function renderScene( sceneState, ); - context.save(); + 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 => { @@ -252,7 +224,8 @@ export function renderScene( ); } }); - context.restore(); + context.fillStyle = fillStyle; + context.strokeStyle = strokeStyle; return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars }; } @@ -317,16 +290,3 @@ export function renderSceneToSvg( ); }); } - -function getContextTransformScaleX(transform: DOMMatrix): number { - return transform.a; -} -function getContextTransformScaleY(transform: DOMMatrix): number { - return transform.d; -} -function getContextTransformTranslateX(transform: DOMMatrix): number { - return transform.e; -} -function getContextTransformTranslateY(transform: DOMMatrix): number { - return transform.f; -} diff --git a/src/scene/export.ts b/src/scene/export.ts index e1f32998..9e03c9b5 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -42,6 +42,7 @@ export function exportToCanvas( elements, appState, null, + scale, rough.canvas(tempCanvas), tempCanvas, { diff --git a/src/scene/index.ts b/src/scene/index.ts index 00ca3a80..d2371a1f 100644 --- a/src/scene/index.ts +++ b/src/scene/index.ts @@ -17,4 +17,4 @@ export { hasText, } from "./comparisons"; export { createScene } from "./createScene"; -export { getZoomOrigin, getZoomTranslation, getNormalizedZoom } from "./zoom"; +export { getZoomOrigin, getNormalizedZoom } from "./zoom"; diff --git a/src/scene/zoom.ts b/src/scene/zoom.ts index 71074d19..ed8ebf1a 100644 --- a/src/scene/zoom.ts +++ b/src/scene/zoom.ts @@ -16,19 +16,6 @@ export function getZoomOrigin(canvas: HTMLCanvasElement | null) { }; } -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)), - }; -} - export function getNormalizedZoom(zoom: number): number { const normalizedZoom = parseFloat(zoom.toFixed(2)); const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2));