Stop using getTransform (#950)

* Stop using getTransform

Fixes #861

The original motivation behind this is to make it work with Firefox. But it also helped make the code more intentional.

Test Plan:
- Create one square, select it, zoom in repeatedly, make sure that it zooms centered in the screen and everything looks good
- Scroll at various zoom levels, things look good
- Export a small scene at 1x and 3x, make sure the background is properly set and look good

* fix selection element
This commit is contained in:
Christopher Chedeau 2020-03-14 17:24:28 -07:00 committed by GitHub
parent dbfc8bee57
commit b20d4539c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 29 additions and 76 deletions

View File

@ -2246,6 +2246,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.state.selectionElement, this.state.selectionElement,
window.devicePixelRatio,
this.rc!, this.rc!,
this.canvas!, this.canvas!,
{ {

View File

@ -303,6 +303,10 @@ export function renderElement(
context.fillStyle = "rgba(0, 0, 255, 0.10)"; context.fillStyle = "rgba(0, 0, 255, 0.10)";
context.fillRect(0, 0, element.width, element.height); context.fillRect(0, 0, element.width, element.height);
context.fillStyle = fillStyle; context.fillStyle = fillStyle;
context.translate(
-element.x - sceneState.scrollX,
-element.y - sceneState.scrollY,
);
break; break;
} }
case "rectangle": case "rectangle":

View File

@ -12,7 +12,6 @@ import {
SCROLLBAR_COLOR, SCROLLBAR_COLOR,
SCROLLBAR_WIDTH, SCROLLBAR_WIDTH,
} from "../scene/scrollbars"; } from "../scene/scrollbars";
import { getZoomTranslation } from "../scene/zoom";
import { getSelectedElements } from "../scene/selection"; import { getSelectedElements } from "../scene/selection";
import { renderElement, renderElementToSvg } from "./renderElement"; import { renderElement, renderElementToSvg } from "./renderElement";
@ -28,6 +27,7 @@ export function renderScene(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
selectionElement: ExcalidrawElement | null, selectionElement: ExcalidrawElement | null,
scale: number,
rc: RoughCanvas, rc: RoughCanvas,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
sceneState: SceneState, sceneState: SceneState,
@ -51,46 +51,11 @@ export function renderScene(
const context = canvas.getContext("2d")!; 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 // When doing calculations based on canvas width we should used normalized one
const normalizedCanvasWidth = const normalizedCanvasWidth = canvas.width / scale;
canvas.width / getContextTransformScaleX(initialContextTransform); const normalizedCanvasHeight = canvas.height / scale;
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();
}
// Paint background // Paint background
context.save();
if (typeof sceneState.viewBackgroundColor === "string") { if (typeof sceneState.viewBackgroundColor === "string") {
const hasTransparence = const hasTransparence =
sceneState.viewBackgroundColor === "transparent" || sceneState.viewBackgroundColor === "transparent" ||
@ -99,12 +64,20 @@ export function renderScene(
if (hasTransparence) { if (hasTransparence) {
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight); context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
} }
const fillStyle = context.fillStyle;
context.fillStyle = sceneState.viewBackgroundColor; context.fillStyle = sceneState.viewBackgroundColor;
context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight); context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight);
context.fillStyle = fillStyle;
} else { } else {
context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight); 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 // Paint visible elements
const visibleElements = elements.filter(element => const visibleElements = elements.filter(element =>
@ -116,15 +89,12 @@ export function renderScene(
), ),
); );
applyZoom(context);
visibleElements.forEach(element => { visibleElements.forEach(element => {
renderElement(element, rc, context, renderOptimizations, sceneState); renderElement(element, rc, context, renderOptimizations, sceneState);
}); });
resetZoom(context);
// Pain selection element // Pain selection element
if (selectionElement) { if (selectionElement) {
applyZoom(context);
renderElement( renderElement(
selectionElement, selectionElement,
rc, rc,
@ -132,7 +102,6 @@ export function renderScene(
renderOptimizations, renderOptimizations,
sceneState, sceneState,
); );
resetZoom(context);
} }
// Pain selected elements // Pain selected elements
@ -140,7 +109,6 @@ export function renderScene(
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
const dashledLinePadding = 4 / sceneState.zoom; const dashledLinePadding = 4 / sceneState.zoom;
applyZoom(context);
context.translate(sceneState.scrollX, sceneState.scrollY); context.translate(sceneState.scrollX, sceneState.scrollY);
selectedElements.forEach(element => { selectedElements.forEach(element => {
const [ const [
@ -166,11 +134,10 @@ export function renderScene(
context.lineWidth = lineWidth; context.lineWidth = lineWidth;
context.setLineDash(initialLineDash); context.setLineDash(initialLineDash);
}); });
resetZoom(context); context.translate(-sceneState.scrollX, -sceneState.scrollY);
// Paint resize handlers // Paint resize handlers
if (selectedElements.length === 1 && selectedElements[0].type !== "text") { if (selectedElements.length === 1 && selectedElements[0].type !== "text") {
applyZoom(context);
context.translate(sceneState.scrollX, sceneState.scrollY); context.translate(sceneState.scrollX, sceneState.scrollY);
context.fillStyle = "#fff"; context.fillStyle = "#fff";
const handlers = handlerRectangles(selectedElements[0], sceneState.zoom); 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.strokeRect(handler[0], handler[1], handler[2], handler[3]);
context.lineWidth = lineWidth; 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 // Paint remote pointers
for (const clientId in sceneState.remotePointerViewportCoords) { for (const clientId in sceneState.remotePointerViewportCoords) {
let { x, y } = sceneState.remotePointerViewportCoords[clientId]; let { x, y } = sceneState.remotePointerViewportCoords[clientId];
@ -237,7 +208,8 @@ export function renderScene(
sceneState, sceneState,
); );
context.save(); const fillStyle = context.fillStyle;
const strokeStyle = context.strokeStyle;
context.fillStyle = SCROLLBAR_COLOR; context.fillStyle = SCROLLBAR_COLOR;
context.strokeStyle = "rgba(255,255,255,0.8)"; context.strokeStyle = "rgba(255,255,255,0.8)";
[scrollBars.horizontal, scrollBars.vertical].forEach(scrollBar => { [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 }; 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;
}

View File

@ -42,6 +42,7 @@ export function exportToCanvas(
elements, elements,
appState, appState,
null, null,
scale,
rough.canvas(tempCanvas), rough.canvas(tempCanvas),
tempCanvas, tempCanvas,
{ {

View File

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

View File

@ -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 { export function getNormalizedZoom(zoom: number): number {
const normalizedZoom = parseFloat(zoom.toFixed(2)); const normalizedZoom = parseFloat(zoom.toFixed(2));
const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2)); const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2));